cocina-models 0.75.0 → 0.76.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +40 -12
  3. data/.rubocop_todo.yml +71 -2
  4. data/README.md +10 -3
  5. data/cocina-models.gemspec +2 -0
  6. data/description_types.yml +165 -38
  7. data/docs/description_types.md +469 -216
  8. data/lib/cocina/generator/generator.rb +7 -12
  9. data/lib/cocina/generator/schema.rb +1 -3
  10. data/lib/cocina/generator/schema_base.rb +0 -8
  11. data/lib/cocina/generator/schema_ref.rb +1 -1
  12. data/lib/cocina/generator/schema_value.rb +14 -4
  13. data/lib/cocina/models/access.rb +4 -4
  14. data/lib/cocina/models/admin_policy.rb +1 -1
  15. data/lib/cocina/models/admin_policy_access_template.rb +7 -7
  16. data/lib/cocina/models/admin_policy_administrative.rb +1 -1
  17. data/lib/cocina/models/admin_policy_with_metadata.rb +3 -3
  18. data/lib/cocina/models/builders/name_title_group_builder.rb +0 -4
  19. data/lib/cocina/models/builders/title_builder.rb +0 -2
  20. data/lib/cocina/models/citation_only_access.rb +2 -2
  21. data/lib/cocina/models/collection_access.rb +4 -4
  22. data/lib/cocina/models/collection_identification.rb +1 -1
  23. data/lib/cocina/models/collection_with_metadata.rb +2 -2
  24. data/lib/cocina/models/contributor.rb +4 -4
  25. data/lib/cocina/models/controlled_digital_lending_access.rb +2 -2
  26. data/lib/cocina/models/dark_access.rb +4 -4
  27. data/lib/cocina/models/description.rb +3 -3
  28. data/lib/cocina/models/descriptive_basic_value.rb +13 -13
  29. data/lib/cocina/models/descriptive_parallel_contributor.rb +5 -5
  30. data/lib/cocina/models/descriptive_parallel_event.rb +3 -3
  31. data/lib/cocina/models/descriptive_value.rb +13 -13
  32. data/lib/cocina/models/descriptive_value_language.rb +6 -6
  33. data/lib/cocina/models/dro.rb +1 -1
  34. data/lib/cocina/models/dro_access.rb +8 -8
  35. data/lib/cocina/models/dro_with_metadata.rb +3 -3
  36. data/lib/cocina/models/embargo.rb +5 -5
  37. data/lib/cocina/models/event.rb +3 -3
  38. data/lib/cocina/models/file.rb +4 -4
  39. data/lib/cocina/models/file_access.rb +4 -4
  40. data/lib/cocina/models/identification.rb +2 -2
  41. data/lib/cocina/models/language.rb +12 -12
  42. data/lib/cocina/models/location_based_access.rb +1 -1
  43. data/lib/cocina/models/location_based_download_access.rb +1 -1
  44. data/lib/cocina/models/mapping/error_notifier.rb +36 -0
  45. data/lib/cocina/models/mapping/from_mods/access.rb +177 -0
  46. data/lib/cocina/models/mapping/from_mods/admin_metadata.rb +217 -0
  47. data/lib/cocina/models/mapping/from_mods/alt_rep_group.rb +26 -0
  48. data/lib/cocina/models/mapping/from_mods/authority.rb +51 -0
  49. data/lib/cocina/models/mapping/from_mods/contributor.rb +161 -0
  50. data/lib/cocina/models/mapping/from_mods/description.rb +99 -0
  51. data/lib/cocina/models/mapping/from_mods/description_builder.rb +61 -0
  52. data/lib/cocina/models/mapping/from_mods/event.rb +543 -0
  53. data/lib/cocina/models/mapping/from_mods/form.rb +381 -0
  54. data/lib/cocina/models/mapping/from_mods/geographic.rb +219 -0
  55. data/lib/cocina/models/mapping/from_mods/hydrus_default_title_builder.rb +28 -0
  56. data/lib/cocina/models/mapping/from_mods/identifier.rb +51 -0
  57. data/lib/cocina/models/mapping/from_mods/identifier_builder.rb +71 -0
  58. data/lib/cocina/models/mapping/from_mods/identifier_type.rb +292 -0
  59. data/lib/cocina/models/mapping/from_mods/language.rb +36 -0
  60. data/lib/cocina/models/mapping/from_mods/language_script.rb +30 -0
  61. data/lib/cocina/models/mapping/from_mods/language_term.rb +106 -0
  62. data/lib/cocina/models/mapping/from_mods/name_builder.rb +307 -0
  63. data/lib/cocina/models/mapping/from_mods/note.rb +162 -0
  64. data/lib/cocina/models/mapping/from_mods/part_builder.rb +147 -0
  65. data/lib/cocina/models/mapping/from_mods/primary.rb +27 -0
  66. data/lib/cocina/models/mapping/from_mods/purl.rb +53 -0
  67. data/lib/cocina/models/mapping/from_mods/related_resource.rb +105 -0
  68. data/lib/cocina/models/mapping/from_mods/subject.rb +413 -0
  69. data/lib/cocina/models/mapping/from_mods/subject_authority_codes.rb +794 -0
  70. data/lib/cocina/models/mapping/from_mods/title.rb +160 -0
  71. data/lib/cocina/models/mapping/from_mods/title_builder.rb +106 -0
  72. data/lib/cocina/models/mapping/from_mods/title_builder_strategy.rb +19 -0
  73. data/lib/cocina/models/mapping/from_mods/value_uri.rb +25 -0
  74. data/lib/cocina/models/mapping/normalizers/base.rb +16 -0
  75. data/lib/cocina/models/mapping/normalizers/mods/geo_extension_normalizer.rb +69 -0
  76. data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +191 -0
  77. data/lib/cocina/models/mapping/normalizers/mods/origin_info_normalizer.rb +157 -0
  78. data/lib/cocina/models/mapping/normalizers/mods/subject_normalizer.rb +296 -0
  79. data/lib/cocina/models/mapping/normalizers/mods/title_normalizer.rb +91 -0
  80. data/lib/cocina/models/mapping/normalizers/mods_normalizer.rb +409 -0
  81. data/lib/cocina/models/mapping/purl.rb +28 -0
  82. data/lib/cocina/models/mapping/to_mods/access.rb +155 -0
  83. data/lib/cocina/models/mapping/to_mods/admin_metadata.rb +129 -0
  84. data/lib/cocina/models/mapping/to_mods/contributor.rb +49 -0
  85. data/lib/cocina/models/mapping/to_mods/description.rb +63 -0
  86. data/lib/cocina/models/mapping/to_mods/event.rb +200 -0
  87. data/lib/cocina/models/mapping/to_mods/form.rb +292 -0
  88. data/lib/cocina/models/mapping/to_mods/geographic.rb +151 -0
  89. data/lib/cocina/models/mapping/to_mods/id_generator.rb +25 -0
  90. data/lib/cocina/models/mapping/to_mods/identifier.rb +57 -0
  91. data/lib/cocina/models/mapping/to_mods/language.rb +82 -0
  92. data/lib/cocina/models/mapping/to_mods/mods_writer.rb +38 -0
  93. data/lib/cocina/models/mapping/to_mods/name_title_group.rb +29 -0
  94. data/lib/cocina/models/mapping/to_mods/name_writer.rb +228 -0
  95. data/lib/cocina/models/mapping/to_mods/note.rb +105 -0
  96. data/lib/cocina/models/mapping/to_mods/part_writer.rb +115 -0
  97. data/lib/cocina/models/mapping/to_mods/related_resource.rb +108 -0
  98. data/lib/cocina/models/mapping/to_mods/role_writer.rb +50 -0
  99. data/lib/cocina/models/mapping/to_mods/subject.rb +486 -0
  100. data/lib/cocina/models/mapping/to_mods/title.rb +260 -0
  101. data/lib/cocina/models/object_metadata.rb +2 -2
  102. data/lib/cocina/models/presentation.rb +2 -2
  103. data/lib/cocina/models/related_resource.rb +9 -9
  104. data/lib/cocina/models/release_tag.rb +4 -4
  105. data/lib/cocina/models/request_admin_policy.rb +1 -1
  106. data/lib/cocina/models/request_administrative.rb +1 -1
  107. data/lib/cocina/models/request_collection.rb +2 -2
  108. data/lib/cocina/models/request_description.rb +3 -3
  109. data/lib/cocina/models/request_dro.rb +4 -4
  110. data/lib/cocina/models/request_file.rb +5 -5
  111. data/lib/cocina/models/request_identification.rb +1 -1
  112. data/lib/cocina/models/sequence.rb +1 -1
  113. data/lib/cocina/models/source.rb +4 -4
  114. data/lib/cocina/models/standard.rb +5 -5
  115. data/lib/cocina/models/stanford_access.rb +2 -2
  116. data/lib/cocina/models/title.rb +13 -13
  117. data/lib/cocina/models/validators/dark_validator.rb +4 -2
  118. data/lib/cocina/models/validators/open_api_validator.rb +0 -4
  119. data/lib/cocina/models/version.rb +1 -1
  120. data/lib/cocina/models/world_access.rb +2 -2
  121. data/lib/cocina/models.rb +4 -0
  122. data/lib/cocina/rspec/factories.rb +157 -0
  123. data/lib/cocina/rspec.rb +2 -0
  124. data/openapi.yml +4 -4
  125. metadata +88 -3
  126. data/docs/_config.yml +0 -1
@@ -5,21 +5,21 @@ module Cocina
5
5
  class Embargo < Struct
6
6
  # Access level.
7
7
  # Validation of this property is relaxed. See the openapi for full validation.
8
- attribute :view, Types::Strict::String.optional.default('dark').meta(omittable: true)
8
+ attribute? :view, Types::Strict::String.optional.default('dark')
9
9
  # Download access level.
10
10
  # Validation of this property is relaxed. See the openapi for full validation.
11
- attribute :download, Types::Strict::String.optional.default('none').meta(omittable: true)
11
+ attribute? :download, Types::Strict::String.optional.default('none')
12
12
  # Not used for this access type, must be null.
13
13
  # Validation of this property is relaxed. See the openapi for full validation.
14
- attribute :location, Types::Strict::String.optional.meta(omittable: true)
14
+ attribute? :location, Types::Strict::String.optional
15
15
  # Validation of this property is relaxed. See the openapi for full validation.
16
- attribute :controlledDigitalLending, Types::Strict::Bool.optional.meta(omittable: true)
16
+ attribute? :controlledDigitalLending, Types::Strict::Bool.optional.default(false)
17
17
  # Date when the Collection is released from an embargo.
18
18
  # example: 2029-06-22T07:00:00.000+00:00
19
19
  attribute :releaseDate, Types::Params::DateTime
20
20
  # The human readable use and reproduction statement that applies when the embargo expires.
21
21
  # example: These materials are in the public domain.
22
- attribute :useAndReproductionStatement, Types::Strict::String.optional.meta(omittable: true)
22
+ attribute? :useAndReproductionStatement, Types::Strict::String.optional
23
23
  end
24
24
  end
25
25
  end
@@ -5,15 +5,15 @@ module Cocina
5
5
  class Event < Struct
6
6
  attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
7
  # Description of the event (creation, publication, etc.).
8
- attribute :type, Types::Strict::String.meta(omittable: true)
8
+ attribute? :type, Types::Strict::String
9
9
  # The preferred display label to use for the event in access systems.
10
- attribute :displayLabel, Types::Strict::String.meta(omittable: true)
10
+ attribute? :displayLabel, Types::Strict::String
11
11
  attribute :date, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
12
12
  attribute :contributor, Types::Strict::Array.of(Contributor).default([].freeze)
13
13
  attribute :location, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
14
14
  attribute :identifier, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
15
15
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
16
- attribute :valueLanguage, DescriptiveValueLanguage.optional.meta(omittable: true)
16
+ attribute? :valueLanguage, DescriptiveValueLanguage.optional
17
17
  attribute :parallelEvent, Types::Strict::Array.of(DescriptiveParallelEvent).default([].freeze)
18
18
  end
19
19
  end
@@ -16,17 +16,17 @@ module Cocina
16
16
  # Filename for a file. Can be same as label.
17
17
  attribute :filename, Types::Strict::String
18
18
  # Size of the File (binary) in bytes.
19
- attribute :size, Types::Strict::Integer.meta(omittable: true)
19
+ attribute? :size, Types::Strict::Integer
20
20
  # Version for the File within SDR.
21
21
  attribute :version, Types::Strict::Integer
22
22
  # MIME Type of the File.
23
- attribute :hasMimeType, Types::Strict::String.meta(omittable: true)
23
+ attribute? :hasMimeType, Types::Strict::String
24
24
  # Use for the File.
25
- attribute :use, Types::Strict::String.meta(omittable: true)
25
+ attribute? :use, Types::Strict::String
26
26
  attribute :hasMessageDigests, Types::Strict::Array.of(MessageDigest).default([].freeze)
27
27
  attribute(:access, FileAccess.default { FileAccess.new })
28
28
  attribute(:administrative, FileAdministrative.default { FileAdministrative.new })
29
- attribute :presentation, Presentation.optional.meta(omittable: true)
29
+ attribute? :presentation, Presentation.optional
30
30
  end
31
31
  end
32
32
  end
@@ -5,15 +5,15 @@ module Cocina
5
5
  class FileAccess < Struct
6
6
  # Access level.
7
7
  # Validation of this property is relaxed. See the openapi for full validation.
8
- attribute :view, Types::Strict::String.optional.default('dark').meta(omittable: true)
8
+ attribute? :view, Types::Strict::String.optional.default('dark')
9
9
  # Download access level.
10
10
  # Validation of this property is relaxed. See the openapi for full validation.
11
- attribute :download, Types::Strict::String.optional.default('none').meta(omittable: true)
11
+ attribute? :download, Types::Strict::String.optional.default('none')
12
12
  # Not used for this access type, must be null.
13
13
  # Validation of this property is relaxed. See the openapi for full validation.
14
- attribute :location, Types::Strict::String.optional.meta(omittable: true)
14
+ attribute? :location, Types::Strict::String.optional
15
15
  # Validation of this property is relaxed. See the openapi for full validation.
16
- attribute :controlledDigitalLending, Types::Strict::Bool.optional.meta(omittable: true)
16
+ attribute? :controlledDigitalLending, Types::Strict::Bool.optional.default(false)
17
17
  end
18
18
  end
19
19
  end
@@ -4,11 +4,11 @@ module Cocina
4
4
  module Models
5
5
  class Identification < 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
  # Digital Object Identifier (https://www.doi.org)
10
10
  # example: 10.25740/bc123df4567
11
- attribute :doi, Types::Strict::String.meta(omittable: true)
11
+ attribute? :doi, Types::Strict::String
12
12
  # 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"
13
13
 
14
14
  # example: sul:PC0170_s3_Fiesta_Bowl_2012-01-02_210609_2026
@@ -5,28 +5,28 @@ module Cocina
5
5
  class Language < Struct
6
6
  attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
7
7
  # Code value of the descriptive element.
8
- attribute :code, Types::Strict::String.meta(omittable: true)
8
+ attribute? :code, Types::Strict::String
9
9
  # The preferred display label to use for the descriptive element in access systems.
10
- attribute :displayLabel, Types::Strict::String.meta(omittable: true)
11
- attribute :encoding, Standard.optional.meta(omittable: true)
10
+ attribute? :displayLabel, Types::Strict::String
11
+ attribute? :encoding, Standard.optional
12
12
  attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
13
13
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
14
14
  attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
15
15
  # present for mapping to additional schemas in the future and for consistency but not otherwise used
16
- attribute :qualifier, Types::Strict::String.meta(omittable: true)
17
- attribute :script, DescriptiveValue.optional.meta(omittable: true)
18
- attribute :source, Source.optional.meta(omittable: true)
16
+ attribute? :qualifier, Types::Strict::String
17
+ attribute? :script, DescriptiveValue.optional
18
+ attribute? :source, Source.optional
19
19
  # Status of the language relative to other parallel language elements (e.g. the primary language)
20
- attribute :status, Types::Strict::String.enum('primary').meta(omittable: true)
21
- attribute :standard, Standard.optional.meta(omittable: true)
20
+ attribute? :status, Types::Strict::String.enum('primary')
21
+ attribute? :standard, Standard.optional
22
22
  attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
23
23
  # URI value of the descriptive element.
24
- attribute :uri, Types::Strict::String.meta(omittable: true)
24
+ attribute? :uri, Types::Strict::String
25
25
  # Value of the descriptive element.
26
- attribute :value, Types::Strict::String.meta(omittable: true)
26
+ attribute? :value, Types::Strict::String
27
27
  # URL or other pointer to the location of the language information.
28
- attribute :valueAt, Types::Strict::String.meta(omittable: true)
29
- attribute :valueLanguage, DescriptiveValueLanguage.optional.meta(omittable: true)
28
+ attribute? :valueAt, Types::Strict::String
29
+ attribute? :valueLanguage, DescriptiveValueLanguage.optional
30
30
  end
31
31
  end
32
32
  end
@@ -9,7 +9,7 @@ module Cocina
9
9
  attribute :download, Types::Strict::String.enum('location-based', 'none')
10
10
  # If access or download is "location-based", which location should have access.
11
11
  attribute :location, Types::Strict::String.enum('spec', 'music', 'ars', 'art', 'hoover', 'm&m')
12
- attribute :controlledDigitalLending, Types::Strict::Bool.enum(false).meta(omittable: true)
12
+ attribute? :controlledDigitalLending, Types::Strict::Bool.default(false).enum(false)
13
13
  end
14
14
  end
15
15
  end
@@ -9,7 +9,7 @@ module Cocina
9
9
  attribute :download, Types::Strict::String.enum('location-based')
10
10
  # Which location should have download access.
11
11
  attribute :location, Types::Strict::String.enum('spec', 'music', 'ars', 'art', 'hoover', 'm&m')
12
- attribute :controlledDigitalLending, Types::Strict::Bool.enum(false).meta(omittable: true)
12
+ attribute? :controlledDigitalLending, Types::Strict::Bool.default(false).enum(false)
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ # Notifies when a data error is encountered. Notification is performed with Honeybadger.
7
+ class ErrorNotifier
8
+ # In addition, it distinguishes between warnings and errors, even if both are notified the same.
9
+ # The determination of warn / error is currently made by the metadata team.
10
+
11
+ # @param [String] druid
12
+ def initialize(druid:)
13
+ @druid = druid
14
+ end
15
+
16
+ # Notify for a non-critical data error.
17
+ # @param [String] message
18
+ # @param [Hash<String, String>] context to add to warning context
19
+ def warn(message, _context = {})
20
+ Kernel.warn "[WARN] #{message}"
21
+ end
22
+
23
+ # Notify for a critical data error.
24
+ # @param [String] message
25
+ # @param [Hash<String, String>] context to add to error context
26
+ def error(message, _context = {})
27
+ Kernel.warn "[ERROR] #{message}"
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :druid
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module FromMods
7
+ # Maps access conditions
8
+ class Access # rubocop:disable Metrics/ClassLength
9
+ ACCESS_CONDITION_TYPES = {
10
+ 'restriction on access' => 'access restriction',
11
+ 'restrictionOnAccess' => 'access restriction',
12
+ 'restrictionsOnAccess' => 'access restriction',
13
+ 'useAndReproduction' => 'use and reproduction'
14
+ }.freeze
15
+
16
+ # @param [Nokogiri::XML::Element] resource_element mods or relatedItem element
17
+ # @param [Cocina::Models::Mapping::FromMods::DescriptionBuilder] description_builder
18
+ # @param [String] purl
19
+ # @return [Hash] a hash that can be mapped to a cocina model
20
+ def self.build(resource_element:, description_builder:, purl: nil)
21
+ new(resource_element: resource_element, description_builder: description_builder, purl: purl).build
22
+ end
23
+
24
+ def initialize(resource_element:, description_builder:, purl:)
25
+ @resource_element = resource_element
26
+ @notifier = description_builder.notifier
27
+ @purl = purl
28
+ end
29
+
30
+ def build
31
+ {}.tap do |access|
32
+ physical_locations = physical_location + shelf_location + xlink_location
33
+ access[:physicalLocation] = physical_locations.presence
34
+ access[:digitalLocation] = digital_location.presence
35
+ access[:accessContact] = access_contact.presence
36
+ access[:url] = url.presence
37
+ access[:note] = (note + purl_note).presence
38
+ end.compact.presence
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :resource_element, :notifier, :add_sdr, :purl
44
+
45
+ # Hydrus is known to create location nodes with no children.
46
+ def location_nodes
47
+ resource_element.xpath('mods:location[*]', mods: Description::DESC_METADATA_NS)
48
+ end
49
+
50
+ def physical_location
51
+ descriptive_value_for(resource_element.xpath(
52
+ "mods:location/mods:physicalLocation[not(@type='repository')][not(@type='discovery')]", mods: Description::DESC_METADATA_NS
53
+ ))
54
+ end
55
+
56
+ def digital_location
57
+ descriptive_value_for(resource_element.xpath(
58
+ "mods:location/mods:physicalLocation[(@type='discovery')]", mods: Description::DESC_METADATA_NS
59
+ ))
60
+ end
61
+
62
+ def xlink_location
63
+ resource_element.xpath('mods:location/mods:physicalLocation[@xlink:href]', mods: Description::DESC_METADATA_NS,
64
+ xlink: Description::XLINK_NS).map do |node|
65
+ {
66
+ valueAt: node['xlink:href']
67
+ }
68
+ end
69
+ end
70
+
71
+ def access_contact
72
+ descriptive_value_for(resource_element.xpath("mods:location/mods:physicalLocation[@type='repository']",
73
+ mods: Description::DESC_METADATA_NS)) +
74
+ descriptive_value_for(resource_element.xpath("mods:note[@type='contact']", mods: Description::DESC_METADATA_NS),
75
+ type: 'email')
76
+ end
77
+
78
+ def shelf_location
79
+ resource_element.xpath('mods:location/mods:shelfLocator',
80
+ mods: Description::DESC_METADATA_NS).filter_map do |shelf_locator_elem|
81
+ next if shelf_locator_elem.content.blank?
82
+
83
+ {
84
+ value: shelf_locator_elem.content,
85
+ type: 'shelf locator'
86
+ }
87
+ end
88
+ end
89
+
90
+ def url
91
+ url_nodes.filter_map do |url_node|
92
+ {
93
+ value: url_node.text.presence,
94
+ displayLabel: url_node[:displayLabel]
95
+ }.tap do |attrs|
96
+ attrs[:status] = 'primary' if url_node == primary_url_node
97
+ attrs[:note] = [{ value: url_node[:note] }] if url_node[:note]
98
+ end.compact.presence
99
+ end
100
+ end
101
+
102
+ def primary_url_node
103
+ all_primary_purl_nodes.first || all_primary_url_nodes.first || this_purl_node || all_purl_nodes.first
104
+ end
105
+
106
+ def this_purl_node
107
+ purl ? all_purl_nodes.find { |purl_node| purl_node.content == purl } : nil
108
+ end
109
+
110
+ def all_primary_url_nodes
111
+ @all_primary_url_nodes ||= all_url_nodes.select { |url_node| url_node[:usage] == 'primary display' }
112
+ end
113
+
114
+ def all_primary_purl_nodes
115
+ @all_primary_purl_nodes ||= all_purl_nodes.select { |purl_node| purl_node[:usage] == 'primary display' }
116
+ end
117
+
118
+ def all_purl_nodes
119
+ @all_purl_nodes ||= all_url_nodes.select { |url_node| Cocina::Models::Mapping::Purl.purl?(url_node.text) }
120
+ end
121
+
122
+ def all_url_nodes
123
+ @all_url_nodes ||= resource_element.xpath('mods:location/mods:url', mods: Description::DESC_METADATA_NS)
124
+ end
125
+
126
+ def primary_purl_node
127
+ @primary_purl_node ||= Purl.primary_purl_node(resource_element, purl)
128
+ end
129
+
130
+ def url_nodes
131
+ @url_nodes ||= all_url_nodes.reject { |url_node| Cocina::Models::Mapping::Purl.purl?(url_node.text) }
132
+ end
133
+
134
+ def purl_note
135
+ return [] unless primary_purl_node
136
+
137
+ Purl.purl_note(primary_purl_node)
138
+ end
139
+
140
+ def note
141
+ resource_element.xpath('mods:accessCondition', mods: Description::DESC_METADATA_NS).map do |access_elem|
142
+ {
143
+ value: access_elem.text.presence,
144
+ type: ACCESS_CONDITION_TYPES.fetch(access_elem['type'], access_elem['type']),
145
+ displayLabel: access_elem['displayLabel'],
146
+ valueAt: access_elem['xlink:href']
147
+ }.compact
148
+ end
149
+ end
150
+
151
+ def descriptive_value_for(nodes, type: nil)
152
+ nodes.filter_map do |node|
153
+ next nil if node.text.blank?
154
+
155
+ {}.tap do |attrs|
156
+ if %w[marcorg oclcorg].include?(node[:authority])
157
+ attrs[:code] = node.text
158
+ else
159
+ attrs[:value] = node.text
160
+ end
161
+ attrs[:uri] = ValueURI.sniff(node[:valueURI], notifier)
162
+ source = {
163
+ code: Authority.normalize_code(node[:authority], notifier),
164
+ uri: Authority.normalize_uri(node[:authorityURI])
165
+ }.compact
166
+ attrs[:source] = source unless source.empty?
167
+ attrs[:type] = type || node[:type]
168
+ attrs[:displayLabel] = node[:displayLabel]
169
+ attrs[:valueLanguage] = LanguageScript.build(node: node)
170
+ end.compact
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module FromMods
7
+ # Maps MODS recordInfo to cocina
8
+ class AdminMetadata # rubocop:disable Metrics/ClassLength
9
+ # @param [Nokogiri::XML::Element] resource_element mods or relatedItem element
10
+ # @param [Cocina::Models::Mapping::FromMods::DescriptionBuilder] description_builder
11
+ # @param [String] purl
12
+ # @return [Hash] a hash that can be mapped to a cocina model
13
+ def self.build(resource_element:, description_builder:, purl: nil)
14
+ new(resource_element: resource_element, description_builder: description_builder).build
15
+ end
16
+
17
+ def initialize(resource_element:, description_builder:)
18
+ @resource_element = resource_element
19
+ @description_builder = description_builder
20
+ @notifier = description_builder.notifier
21
+ end
22
+
23
+ def build
24
+ return nil if record_info.nil?
25
+
26
+ {}.tap do |admin_metadata|
27
+ admin_metadata[:language] = build_language
28
+ admin_metadata[:contributor] = build_contributor
29
+ admin_metadata[:metadataStandard] = build_standard
30
+ admin_metadata[:note] = build_note
31
+ admin_metadata[:identifier] = build_identifier
32
+ admin_metadata[:event] = build_events
33
+ end.compact
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :resource_element, :notifier, :description_builder
39
+
40
+ def build_events
41
+ events = []
42
+ events << build_event_for(creation_event, 'creation') if creation_event
43
+ modification_events.each do |event|
44
+ events << build_event_for(event, 'modification')
45
+ end
46
+ return nil if events.empty?
47
+
48
+ events
49
+ end
50
+
51
+ def build_event_for(node, type)
52
+ event_code = node['encoding']
53
+ encoding = { code: event_code } if event_code
54
+ {
55
+ type: type,
56
+ date: [
57
+ {
58
+ value: node.text,
59
+ encoding: encoding
60
+ }.compact
61
+ ]
62
+ }
63
+ end
64
+
65
+ def build_identifier
66
+ identifiers = record_identifiers.map do |identifier|
67
+ IdentifierBuilder.build_from_record_identifier(identifier_element: identifier)
68
+ end
69
+
70
+ return nil if identifiers.empty?
71
+
72
+ identifiers
73
+ end
74
+
75
+ def build_note
76
+ notes = []
77
+ record_origins.each do |record_origin|
78
+ notes << {
79
+ type: 'record origin',
80
+ value: record_origin.text
81
+ }
82
+ end
83
+ record_info_notes.each do |info_note|
84
+ notes << if info_note['xlink:href']
85
+ { valueAt: info_note['xlink:href'] }
86
+ else
87
+ {
88
+ type: 'record information',
89
+ value: info_note.text
90
+ }
91
+ end
92
+ end
93
+ notes.presence
94
+ end
95
+
96
+ def build_standard
97
+ return unless description_standards
98
+
99
+ description_standards.map do |description_standard|
100
+ source = {
101
+ uri: Authority.normalize_uri(description_standard['authorityURI']),
102
+ code: description_standard['authority']
103
+ }.compact
104
+ {
105
+ uri: ValueURI.sniff(description_standard['valueURI'], notifier),
106
+ source: source.presence
107
+ }.tap do |attrs|
108
+ if description_standard.text.present?
109
+ if description_standard.text == description_standard.text.downcase
110
+ attrs[:code] = description_standard.text
111
+ else
112
+ attrs[:value] = description_standard.text
113
+ end
114
+ end
115
+ end.compact
116
+ end.presence
117
+ end
118
+
119
+ def build_contributor
120
+ record_content_sources.map do |record_content_source|
121
+ if record_content_source['authority'] == 'marcorg'
122
+ build_contributor_code(record_content_source)
123
+ else
124
+ build_contributor_value(record_content_source)
125
+ end
126
+ end.presence
127
+ end
128
+
129
+ def build_contributor_value(record_content_source)
130
+ {
131
+ name: [
132
+ {
133
+ value: record_content_source.text,
134
+ uri: ValueURI.sniff(record_content_source['valueURI'], notifier),
135
+ source: source_for(record_content_source)
136
+
137
+ }.compact
138
+ ]
139
+ }
140
+ end
141
+
142
+ def build_contributor_code(record_content_source)
143
+ {
144
+ name: [
145
+ {
146
+ code: record_content_source.text,
147
+ uri: ValueURI.sniff(record_content_source['valueURI'], notifier),
148
+ source: source_for(record_content_source)
149
+ }.compact
150
+ ],
151
+ type: 'organization',
152
+ role: [
153
+ {
154
+ value: 'original cataloging agency'
155
+ }
156
+ ]
157
+ }
158
+ end
159
+
160
+ def source_for(record_content_source)
161
+ {
162
+ code: record_content_source['authority'],
163
+ uri: record_content_source['authorityURI']
164
+ }.compact.presence
165
+ end
166
+
167
+ def build_language
168
+ return if language_of_cataloging.empty?
169
+
170
+ language_of_cataloging.map do |lang_node|
171
+ Cocina::Models::Mapping::FromMods::LanguageTerm.build(
172
+ language_element: lang_node,
173
+ notifier: notifier
174
+ )
175
+ end
176
+ end
177
+
178
+ def record_info
179
+ @record_info ||= resource_element.xpath('mods:recordInfo[1]', mods: Description::DESC_METADATA_NS).first
180
+ end
181
+
182
+ def language_of_cataloging
183
+ @language_of_cataloging ||= record_info.xpath('mods:languageOfCataloging', mods: Description::DESC_METADATA_NS)
184
+ end
185
+
186
+ def record_content_sources
187
+ @record_content_sources ||= record_info.xpath('mods:recordContentSource', mods: Description::DESC_METADATA_NS)
188
+ end
189
+
190
+ def description_standards
191
+ @description_standards ||= record_info.xpath('mods:descriptionStandard', mods: Description::DESC_METADATA_NS)
192
+ end
193
+
194
+ def record_origins
195
+ @record_origins ||= record_info.xpath('mods:recordOrigin', mods: Description::DESC_METADATA_NS)
196
+ end
197
+
198
+ def record_info_notes
199
+ @record_info_notes ||= record_info.xpath('mods:recordInfoNote', mods: Description::DESC_METADATA_NS)
200
+ end
201
+
202
+ def creation_event
203
+ @creation_event ||= record_info.xpath('mods:recordCreationDate', mods: Description::DESC_METADATA_NS).first
204
+ end
205
+
206
+ def modification_events
207
+ @modification_events ||= record_info.xpath('mods:recordChangeDate', mods: Description::DESC_METADATA_NS)
208
+ end
209
+
210
+ def record_identifiers
211
+ @record_identifiers ||= record_info.xpath('mods:recordIdentifier', mods: Description::DESC_METADATA_NS)
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module FromMods
7
+ # Helper class: splits nodes by altRepGroup ids
8
+ class AltRepGroup
9
+ # @param [Array<Nokogiri::XML::Element>] nodes to split
10
+ # @return [Array<Array<Nokogiri::XML::Element>>, Array<Nokogiri::XML::Element>] nodes grouped by altRepGroup, other nodes
11
+ def self.split(nodes:)
12
+ all_nodes_with_altrepgroup = nodes.reject { |node| node[:altRepGroup].blank? }
13
+ grouped_altrepgroup_nodes = all_nodes_with_altrepgroup
14
+ .group_by { |node| node[:altRepGroup] }
15
+ .values
16
+ .reject { |group_nodes| group_nodes.size == 1 }
17
+
18
+ other_nodes = nodes.reject { |node| grouped_altrepgroup_nodes.flatten.include?(node) }
19
+
20
+ [grouped_altrepgroup_nodes, other_nodes]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end