cocina-models 0.78.0 → 0.81.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/description_types.yml +2 -0
  3. data/docs/description_types.md +1 -0
  4. data/lib/cocina/generator/schema.rb +2 -2
  5. data/lib/cocina/generator/schema_base.rb +10 -0
  6. data/lib/cocina/generator/schema_value.rb +0 -4
  7. data/lib/cocina/models/access_role.rb +1 -0
  8. data/lib/cocina/models/access_role_member.rb +1 -0
  9. data/lib/cocina/models/admin_policy_access_template.rb +1 -0
  10. data/lib/cocina/models/admin_policy_administrative.rb +1 -0
  11. data/lib/cocina/models/admin_policy_with_metadata.rb +1 -0
  12. data/lib/cocina/models/applies_to.rb +1 -0
  13. data/lib/cocina/models/builders/name_title_group_builder.rb +0 -1
  14. data/lib/cocina/models/collection.rb +1 -0
  15. data/lib/cocina/models/collection_access.rb +1 -0
  16. data/lib/cocina/models/collection_with_metadata.rb +1 -0
  17. data/lib/cocina/models/contributor.rb +1 -0
  18. data/lib/cocina/models/descriptive_access_metadata.rb +1 -0
  19. data/lib/cocina/models/descriptive_admin_metadata.rb +1 -0
  20. data/lib/cocina/models/descriptive_basic_value.rb +1 -0
  21. data/lib/cocina/models/descriptive_geographic_metadata.rb +1 -0
  22. data/lib/cocina/models/descriptive_grouped_value.rb +1 -0
  23. data/lib/cocina/models/descriptive_parallel_contributor.rb +2 -0
  24. data/lib/cocina/models/descriptive_parallel_event.rb +1 -0
  25. data/lib/cocina/models/descriptive_parallel_value.rb +1 -0
  26. data/lib/cocina/models/descriptive_structured_value.rb +1 -0
  27. data/lib/cocina/models/descriptive_value.rb +1 -0
  28. data/lib/cocina/models/descriptive_value_language.rb +1 -0
  29. data/lib/cocina/models/dro.rb +1 -0
  30. data/lib/cocina/models/dro_structural.rb +1 -0
  31. data/lib/cocina/models/dro_with_metadata.rb +1 -0
  32. data/lib/cocina/models/event.rb +1 -0
  33. data/lib/cocina/models/file.rb +1 -0
  34. data/lib/cocina/models/file_access.rb +1 -0
  35. data/lib/cocina/models/file_set.rb +1 -0
  36. data/lib/cocina/models/file_set_structural.rb +1 -0
  37. data/lib/cocina/models/geographic.rb +1 -0
  38. data/lib/cocina/models/language.rb +1 -0
  39. data/lib/cocina/models/mapping/from_mods/name_builder.rb +79 -23
  40. data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +27 -2
  41. data/lib/cocina/models/mapping/to_mods/name_writer.rb +79 -21
  42. data/lib/cocina/models/mapping/to_mods/subject.rb +2 -2
  43. data/lib/cocina/models/message_digest.rb +1 -0
  44. data/lib/cocina/models/object_metadata.rb +1 -0
  45. data/lib/cocina/models/presentation.rb +1 -0
  46. data/lib/cocina/models/related_resource.rb +1 -0
  47. data/lib/cocina/models/release_tag.rb +1 -0
  48. data/lib/cocina/models/request_admin_policy.rb +1 -0
  49. data/lib/cocina/models/request_collection.rb +1 -0
  50. data/lib/cocina/models/request_description.rb +1 -0
  51. data/lib/cocina/models/request_dro.rb +1 -0
  52. data/lib/cocina/models/request_dro_structural.rb +1 -0
  53. data/lib/cocina/models/request_file_set_structural.rb +1 -0
  54. data/lib/cocina/models/request_identification.rb +1 -0
  55. data/lib/cocina/models/sequence.rb +1 -0
  56. data/lib/cocina/models/source.rb +1 -0
  57. data/lib/cocina/models/standard.rb +1 -0
  58. data/lib/cocina/models/validators/associated_name_validator.rb +2 -4
  59. data/lib/cocina/models/validators/description_types_validator.rb +2 -3
  60. data/lib/cocina/models/validators/description_values_validator.rb +2 -3
  61. data/lib/cocina/models/validators/validator.rb +1 -2
  62. data/lib/cocina/models/version.rb +1 -1
  63. data/lib/cocina/models.rb +2 -2
  64. data/lib/cocina/rspec/factories.rb +13 -6
  65. data/openapi.yml +1 -0
  66. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09a3e28b24593aeb0e75c0318067e72fc8e10bb98e189d00b3f166357b198cab'
4
- data.tar.gz: 688dbfe3ef449fe8caccd5c3518a6cd059cff2920ad9af3d9fb11553516b520e
3
+ metadata.gz: 0d8392fadece0bb4ec12ed93e958c99abf04e29ad0a78c14ee8651ec8ecd1e37
4
+ data.tar.gz: db4a5de344bb53d98b2b71f725e8b1d1b9e0d5e0b28416989ad55f19df826376
5
5
  SHA512:
6
- metadata.gz: 3bc1bce3e8624fa829ce0f41b69362d723e77549d102d561b41245c462c178cbcac5733fcd0816fd7750fa7bf736611e46de503e1e6b93dfce009ae6bd33eea4
7
- data.tar.gz: 5cb526603a08485e0c04abccfa962b92b890fbf9d966d5dc00cb5ddd9aec64b24ebb4aa555397d769d5814edb914960d7d83a1b7f5c23be6752252f34b64a033
6
+ metadata.gz: 4880e4eb86935116e0bec017926aeab5e5aa12d8792ee5066d0aeef5f514e6f13f63c8bc68c03035fdff2748f95aa7215f236384e109a8e532bd76e9677ae53c
7
+ data.tar.gz: 22154804ee4b103bdbd3aaaee449327bb4a6c9bc1dce9431a5311ed06c5e76039ccb420634d275f56d644a60ffa529bd9cb822f9c549be057f4972d791a34bf7
@@ -391,6 +391,8 @@ identifier:
391
391
  - value: record id
392
392
  - value: Senate Number
393
393
  - value: Series
394
+ - value: SICI
395
+ code: sici
394
396
  - value: SIRSI
395
397
  - value: Source ID
396
398
  - value: sourceID
@@ -385,6 +385,7 @@ _Path: identifier.type_
385
385
  * record id
386
386
  * Senate Number
387
387
  * Series
388
+ * SICI
388
389
  * SIRSI
389
390
  * Source ID
390
391
  * sourceID
@@ -13,8 +13,8 @@ module Cocina
13
13
  # frozen_string_literal: true
14
14
 
15
15
  module Cocina
16
- module Models
17
- class #{name} < Struct
16
+ module Models#{' '}
17
+ #{preamble}class #{name} < Struct
18
18
 
19
19
  #{validate}
20
20
  #{types}
@@ -41,6 +41,12 @@ module Cocina
41
41
  "# #{schema_doc.description}\n"
42
42
  end
43
43
 
44
+ def deprecation
45
+ return '' unless schema_doc.deprecated?
46
+
47
+ "# DEPRECATED\n"
48
+ end
49
+
44
50
  def example
45
51
  return '' unless schema_doc.example
46
52
 
@@ -82,6 +88,10 @@ module Cocina
82
88
  'Strict::String'
83
89
  end
84
90
  end
91
+
92
+ def preamble
93
+ "#{deprecation}#{description}#{example}#{relaxed_comment}"
94
+ end
85
95
  end
86
96
  end
87
97
  end
@@ -19,10 +19,6 @@ module Cocina
19
19
  "Types::#{dry_datatype(schema_doc)}#{optional}#{default}#{enum}"
20
20
  end
21
21
 
22
- def preamble
23
- "#{description}#{example}#{relaxed_comment}"
24
- end
25
-
26
22
  def enum
27
23
  return '' if !schema_doc.enum || relaxed
28
24
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Access role conferred by an AdminPolicy to objects within it. (used by Argo)
5
6
  class AccessRole < Struct
6
7
  # Name of role
7
8
  attribute :name, Types::Strict::String.enum('dor-apo-depositor', 'dor-apo-manager', 'dor-apo-viewer', 'sdr-administrator', 'sdr-viewer', 'hydrus-collection-creator', 'hydrus-collection-manager', 'hydrus-collection-depositor', 'hydrus-collection-item-depositor', 'hydrus-collection-reviewer', 'hydrus-collection-viewer')
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Represents a user or group that is a member of an AccessRole
5
6
  class AccessRoleMember < Struct
6
7
  include Checkable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Provides the template of access settings that is copied to the items goverend by an AdminPolicy. This is almost the same as DROAccess, but it provides no defaults and has no embargo.
5
6
  class AdminPolicyAccessTemplate < Struct
6
7
  attribute? :view, Types::Strict::String.enum('world', 'stanford', 'location-based', 'citation-only', 'dark')
7
8
  # Available for controlled digital lending.
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Administrative properties for an AdminPolicy
5
6
  class AdminPolicyAdministrative < Struct
6
7
  attribute(:accessTemplate, AdminPolicyAccessTemplate.default { AdminPolicyAccessTemplate.new })
7
8
  attribute :registrationWorkflow, Types::Strict::Array.of(Types::Strict::String).default([].freeze)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Admin Policy with addition object metadata.
5
6
  class AdminPolicyWithMetadata < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Property model for indicating the parts, aspects, or versions of the resource to which a descriptive element is applicable.
5
6
  class AppliesTo < Struct
6
7
  attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
7
8
  end
@@ -92,7 +92,6 @@ module Cocina
92
92
  # ignoring groupedValue
93
93
  slices.flatten
94
94
  end
95
- # private_class_method :value_slices
96
95
 
97
96
  # for a given Hash (from a Cocina DescriptiveValue or Title or Name or ...)
98
97
  # result will be either
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # A group of Digital Repository Objects that indicate some type of conceptual grouping within the domain that is worth reusing across the system.
5
6
  class Collection < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Access metadata for collections
5
6
  class CollectionAccess < Struct
6
7
  # Access level
7
8
  attribute? :view, Types::Strict::String.default('dark').enum('world', 'dark')
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Collection with addition object metadata.
5
6
  class CollectionWithMetadata < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Property model for describing agents contributing in some way to the creation and history of the resource.
5
6
  class Contributor < Struct
6
7
  attribute :name, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  # Entity type of the contributor (person, organization, etc.). See https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md for valid types.
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Information about how to access digital and physical versions of the object.
5
6
  class DescriptiveAccessMetadata < Struct
6
7
  attribute :url, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  attribute :physicalLocation, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Information about this resource description.
5
6
  class DescriptiveAdminMetadata < Struct
6
7
  attribute :contributor, Types::Strict::Array.of(Contributor).default([].freeze)
7
8
  attribute :event, Types::Strict::Array.of(Event).default([].freeze)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Basic value model for descriptive elements. Can only have one of value, parallelValue, groupedValue, or structuredValue.
5
6
  class DescriptiveBasicValue < Struct
6
7
  attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Value model for mods geographic extension metadata
5
6
  class DescriptiveGeographicMetadata < Struct
6
7
  attribute :form, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  attribute :subject, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Value model for a set of descriptive elements grouped together in an unstructured way.
5
6
  class DescriptiveGroupedValue < Struct
6
7
  attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # DEPRECATED
6
+ # Value model for multiple representations of information about the same contributor (e.g. in different languages).
5
7
  class DescriptiveParallelContributor < Struct
6
8
  attribute :name, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
9
  # Entity type of the contributor (person, organization, etc.). See https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md for valid types.
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Value model for multiple representations of information about the same event (e.g. in different languages).
5
6
  class DescriptiveParallelEvent < Struct
6
7
  attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  # Description of the event (creation, publication, etc.).
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Value model for multiple representations of the same information (e.g. in different languages).
5
6
  class DescriptiveParallelValue < Struct
6
7
  attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Value model for descriptive elements structured as typed, ordered values.
5
6
  class DescriptiveStructuredValue < Struct
6
7
  attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Default value model for descriptive elements.
5
6
  class DescriptiveValue < Struct
6
7
  attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Language of the descriptive element value
5
6
  class DescriptiveValueLanguage < Struct
6
7
  # Code representing the standard or encoding.
7
8
  attribute? :code, Types::Strict::String
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Domain-defined abstraction of a 'work'. Digital Repository Objects' abstraction is describable for our domain’s purposes, i.e. for management needs within our system.
5
6
  class DRO < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Structural metadata
5
6
  class DROStructural < Struct
6
7
  attribute :contains, Types::Strict::Array.of(FileSet).default([].freeze)
7
8
  attribute :hasMemberOrders, Types::Strict::Array.of(Sequence).default([].freeze)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # DRO with addition object metadata.
5
6
  class DROWithMetadata < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Property model for describing events in the history of the resource.
5
6
  class Event < Struct
6
7
  attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
7
8
  # Description of the event (creation, publication, etc.).
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Binaries that are the basis of what our domain manages. Binaries here do not include metadata files generated for the domain's own management purposes.
5
6
  class File < Struct
6
7
  include Checkable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Access metadata for files
5
6
  class FileAccess < Struct
6
7
  # Access level.
7
8
  # Validation of this property is relaxed. See the openapi for full validation.
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Relevant groupings of Files. Also called a File Grouping.
5
6
  class FileSet < Struct
6
7
  include Checkable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Structural metadata
5
6
  class FileSetStructural < Struct
6
7
  attribute :contains, Types::Strict::Array.of(File).default([].freeze)
7
8
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Geographic metadata
5
6
  class Geographic < Struct
6
7
  # Geographic ISO 19139 XML metadata
7
8
  attribute :iso19139, Types::Strict::String
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Languages, scripts, symbolic systems, and notations used in all or part of a resource or its descriptive metadata.
5
6
  class Language < Struct
6
7
  attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
7
8
  # Code value of the descriptive element.
@@ -36,13 +36,13 @@ module Cocina
36
36
 
37
37
  def build_parallel
38
38
  names = {
39
- parallelValue: name_elements.map { |name_node| build_parallel_name(name_node) },
39
+ parallelValue: build_parallel_values,
40
40
  type: type_for(name_elements.first['type']),
41
41
  status: name_elements.filter_map { |name_element| name_element['usage'] }.first
42
42
  }.compact
43
43
  { name: [names] }.tap do |attrs|
44
- roles = name_elements.flat_map { |name_node| build_roles(name_node) }.compact.uniq
45
- attrs[:role] = roles.presence
44
+ attrs[:role] = name_elements.flat_map { |name_node| build_roles(name_node) }.compact.uniq.presence
45
+ attrs[:note] = name_elements.flat_map { |name_node| build_affiliation_notes(name_node) }.compact.uniq.presence
46
46
  end.compact
47
47
  end
48
48
 
@@ -51,8 +51,8 @@ module Cocina
51
51
  status: name_node['usage']
52
52
  }.compact.merge(common_lang_script(name_node))
53
53
 
54
- name_attrs = name_attrs.merge(common_name(name_node, name_attrs[:name]))
55
- name_parts = build_name_parts(name_node)
54
+ name_attrs = name_attrs.merge(common_name(name_node, name_attrs[:name], is_parallel: true))
55
+ name_parts = build_name_parts(name_node, parallel: true)
56
56
  notifier.warn('Missing name/namePart element') if name_parts.all?(&:empty?)
57
57
  name_parts.each { |name_part| name_attrs = name_part.merge(name_attrs) }
58
58
  name_attrs.compact
@@ -85,9 +85,9 @@ module Cocina
85
85
  }.compact.merge(common_name(name_node, name_parts))
86
86
  end
87
87
 
88
- def common_name(name_node, name)
88
+ def common_name(name_node, name, is_parallel: false)
89
89
  {
90
- note: build_notes(name_node),
90
+ note: build_notes(name_node, is_parallel: is_parallel),
91
91
  identifier: build_identifier(name_node)
92
92
  }.tap do |attrs|
93
93
  roles = build_roles(name_node)
@@ -106,7 +106,17 @@ module Cocina
106
106
  end.compact
107
107
  end
108
108
 
109
- def build_name_parts(name_node)
109
+ def build_parallel_values
110
+ parallel_values = []
111
+ name_elements.each do |name_node|
112
+ parallel_values << build_parallel_name(name_node)
113
+ display_val = display_value(name_node)
114
+ parallel_values << display_val if display_val
115
+ end
116
+ parallel_values.compact
117
+ end
118
+
119
+ def build_name_parts(name_node, parallel: false)
110
120
  name_part_nodes = name_node.xpath('mods:namePart', mods: Description::DESC_METADATA_NS)
111
121
  alternative_name_nodes = name_node.xpath('mods:alternativeName', mods: Description::DESC_METADATA_NS)
112
122
 
@@ -116,23 +126,64 @@ module Cocina
116
126
  parts << { valueAt: name_node['xlink:href'] } if name_node['xlink:href']
117
127
  parts << common_authority(name_node) if name_node['valueURI']
118
128
  when 1
119
- parts << build_name_part(name_node, name_part_nodes.first,
120
- default_type: alternative_name_nodes.present?)
121
- .merge(common_authority(name_node)).merge(common_lang_script(name_node)).presence
129
+ name = build_simple_value_name(name_node, name_part_nodes.first, alternative_name_nodes, parallel)
130
+ parts << name.merge(common_authority(name_node)).merge(common_lang_script(name_node)).presence
122
131
  else
123
- vals = name_part_nodes.filter_map do |name_part|
124
- build_name_part(name_node, name_part, default_type: name_node['type'] != 'corporate').presence
125
- end
126
- parts << { structuredValue: vals }.merge(common_authority(name_node)).merge(common_lang_script(name_node))
132
+ name = build_structured_value_name(name_node, name_part_nodes)
133
+ parts << name.merge(common_authority(name_node)).merge(common_lang_script(name_node))
127
134
  end
128
135
 
129
136
  parts = build_alternative_name(alternative_name_nodes, parts) if alternative_name_nodes.present?
130
-
131
- display_form = name_node.xpath('mods:displayForm', mods: Description::DESC_METADATA_NS).first
132
- parts << { value: display_form.text, type: 'display' } if display_form
133
137
  parts.compact
134
138
  end
135
139
 
140
+ def build_simple_value_name(name_node, name_part_node, alternative_name_nodes, parallel)
141
+ name_value_hash = build_name_part(name_node, name_part_node, default_type: alternative_name_nodes.present?)
142
+ display_form_node = name_node.xpath('mods:displayForm', mods: Description::DESC_METADATA_NS).first
143
+ if display_form_node.present? && !parallel
144
+ cocina_contrib_name =
145
+ {
146
+ parallelValue: [
147
+ name_value_hash
148
+ ]
149
+ }
150
+ add_display_parallel_value(name_node, cocina_contrib_name)
151
+ else
152
+ name_value_hash
153
+ end
154
+ end
155
+
156
+ def build_structured_value_name(name_node, name_part_nodes)
157
+ vals = name_part_nodes.filter_map { |name_part_node| build_name_part(name_node, name_part_node, default_type: name_node['type'] != 'corporate').presence }
158
+ display_form_node = name_node.xpath('mods:displayForm', mods: Description::DESC_METADATA_NS).first
159
+ if display_form_node.present?
160
+ cocina_contrib_name =
161
+ {
162
+ parallelValue: [
163
+ { structuredValue: vals }
164
+ ]
165
+ }
166
+ add_display_parallel_value(name_node, cocina_contrib_name)
167
+ else
168
+ { structuredValue: vals }
169
+ end
170
+ end
171
+
172
+ def add_display_parallel_value(name_node, cocina_contrib_name)
173
+ display_form = name_node.xpath('mods:displayForm', mods: Description::DESC_METADATA_NS)&.text
174
+ return cocina_contrib_name if display_form.blank?
175
+
176
+ display_parallel_value = display_value(name_node)
177
+ cocina_contrib_name[:parallelValue] << display_parallel_value if display_parallel_value && cocina_contrib_name[:parallelValue].present?
178
+
179
+ cocina_contrib_name
180
+ end
181
+
182
+ def display_value(name_node)
183
+ display_form = name_node.xpath('mods:displayForm', mods: Description::DESC_METADATA_NS)&.text
184
+ { value: display_form, type: 'display' } if display_form.present?
185
+ end
186
+
136
187
  def build_name_part(name_node, name_part_node, default_type: true)
137
188
  if name_part_node.content.blank? && !name_part_node['xlink:href']
138
189
  notifier.warn('name/namePart missing value')
@@ -224,12 +275,8 @@ module Cocina
224
275
  end.presence
225
276
  end
226
277
 
227
- def build_notes(name_node)
278
+ def build_notes(name_node, is_parallel:)
228
279
  [].tap do |parts|
229
- name_node.xpath('mods:affiliation', mods: Description::DESC_METADATA_NS).each do |affiliation_node|
230
- parts << { value: affiliation_node.text, type: 'affiliation' }
231
- end
232
-
233
280
  description = name_node.xpath('mods:description', mods: Description::DESC_METADATA_NS).first
234
281
  if description
235
282
  parts << if description.text == UNCITED_DESCRIPTION
@@ -238,9 +285,18 @@ module Cocina
238
285
  { value: description.text, type: 'description' }
239
286
  end
240
287
  end
288
+ parts.concat(build_affiliation_notes(name_node)) unless is_parallel
241
289
  end.presence
242
290
  end
243
291
 
292
+ def build_affiliation_notes(name_node)
293
+ [].tap do |parts|
294
+ name_node.xpath('mods:affiliation', mods: Description::DESC_METADATA_NS).each do |affiliation_node|
295
+ parts << { value: affiliation_node.text, type: 'affiliation' }
296
+ end
297
+ end
298
+ end
299
+
244
300
  def build_roles(name_node)
245
301
  role_nodes = name_node.xpath('mods:role', mods: Description::DESC_METADATA_NS)
246
302
  role_nodes.filter_map { |role_node| role_for(role_node) }.presence
@@ -20,6 +20,7 @@ module Cocina
20
20
 
21
21
  def normalize
22
22
  normalize_parallel_name_role
23
+ normalize_parallel_affiliation_note
23
24
  normalize_text_role_term
24
25
  normalize_role_term
25
26
  normalize_role # must be after normalize_role_term
@@ -35,10 +36,13 @@ module Cocina
35
36
 
36
37
  attr_reader :ng_xml
37
38
 
39
+ def grouped_name_nodes
40
+ name_nodes = ng_xml.root.xpath('//mods:name[@altRepGroup]', mods: ModsNormalizer::MODS_NS)
41
+ name_nodes.group_by { |name_node| name_node['altRepGroup'] }.values.reject { |name_node_group| name_node_group.size == 1 }
42
+ end
43
+
38
44
  def normalize_parallel_name_role
39
45
  # For parallel names, all should have the same roles.
40
- name_nodes = ng_xml.root.xpath('//mods:name[@altRepGroup]', mods: ModsNormalizer::MODS_NS)
41
- grouped_name_nodes = name_nodes.group_by { |name_node| name_node['altRepGroup'] }.values.reject { |name_node_group| name_node_group.size == 1 }
42
46
  grouped_name_nodes.each do |name_node_group|
43
47
  name_node_with_role = name_node_group.find { |name_node| role_node_for(name_node) }
44
48
  next unless name_node_with_role
@@ -58,6 +62,27 @@ module Cocina
58
62
  name_node.xpath('mods:role', mods: ModsNormalizer::MODS_NS).first
59
63
  end
60
64
 
65
+ def normalize_parallel_affiliation_note
66
+ # For parallel names, all should have the same affiliation.
67
+ grouped_name_nodes.each do |name_node_group|
68
+ name_node_with_affiliation = name_node_group.find { |name_node| affiliation_node_for(name_node) }
69
+ next unless name_node_with_affiliation
70
+
71
+ name_node_group.each do |name_node|
72
+ next if name_node == name_node_with_affiliation
73
+
74
+ existing_affiliation_node = affiliation_node_for(name_node)
75
+ existing_affiliation_node&.remove
76
+
77
+ name_node << affiliation_node_for(name_node_with_affiliation).dup
78
+ end
79
+ end
80
+ end
81
+
82
+ def affiliation_node_for(name_node)
83
+ name_node.xpath('mods:affiliation', mods: ModsNormalizer::MODS_NS).first
84
+ end
85
+
61
86
  def normalize_text_role_term
62
87
  # Add the type="text" attribute to roleTerms that don't have a type (seen in MODS 3.3 druid:yy910cj7795)
63
88
  ng_xml.root.xpath('//mods:roleTerm[not(@type)]', mods: ModsNormalizer::MODS_NS).each do |role_term_node|
@@ -36,25 +36,11 @@ module Cocina
36
36
  if contributor.type == 'unspecified others'
37
37
  write_etal
38
38
  elsif contributor.name.present?
39
+ # Expect contributor to have a single value for name property
39
40
  contrib_name = contributor.name.first
40
- parallel_values = contrib_name.parallelValue
41
- if parallel_values.present?
42
- altrepgroup_id = id_generator.next_altrepgroup
43
- parallel_values.each do |parallel_contrib_name|
44
- Cocina::Models::Builders::NameTitleGroupBuilder.value_slices(parallel_contrib_name)&.each do |parallel_contrib_name_slice|
45
- if name_title_vals_index[parallel_contrib_name_slice]
46
- name_title_group = name_title_vals_index[parallel_contrib_name_slice]&.values&.first
47
- write_parallel_contributor(contributor, contrib_name, parallel_contrib_name,
48
- name_title_group, altrepgroup_id)
49
- else
50
- # TODO: want a way to notify that we hit a problem - either notifier or HB error (issue #3751)
51
- # OR validate for semantic correctness upon creation/update so we can't get here.
52
- # notifier.warn("For contributor name '#{parallel_contrib_name_val}', no title matching '#{title_from_contrib}'")
53
- write_parallel_contributor(contributor, contrib_name, parallel_contrib_name, nil,
54
- altrepgroup_id)
55
- end
56
- end
57
- end
41
+ parallel_name_values = contrib_name.parallelValue
42
+ if parallel_name_values.present?
43
+ write_contributor_with_parallel_names(parallel_name_values, contrib_name)
58
44
  else
59
45
  write_contributor(contributor)
60
46
  end
@@ -105,7 +91,50 @@ module Cocina
105
91
  end
106
92
  end
107
93
 
108
- def write_parallel_contributor(contributor, name, parallel_name, name_title_group, altrepgroup_id)
94
+ def write_contributor_with_parallel_names(parallel_name_values, contrib_name)
95
+ display_type_parallel_name = display_type_parallel_name(parallel_name_values)
96
+ if parallel_name_values.size == 1
97
+ contrib_name_value_slice = Cocina::Models::Builders::NameTitleGroupBuilder.value_slices(parallel_name_values.first)
98
+ name_title_group = name_title_vals_index[contrib_name_value_slice]&.values&.first
99
+ write_name_from_parallel(contributor, contributor.name.first, parallel_name_values, name_title_group, nil)
100
+ elsif parallel_name_values.size == 2 && display_type_parallel_name
101
+ contrib_name_value_slice = Cocina::Models::Builders::NameTitleGroupBuilder.value_slices(parallel_name_values.first)
102
+ name_title_group = name_title_vals_index[contrib_name_value_slice]&.values&.first
103
+ write_name_with_display_form(contributor, contributor.name.first, parallel_name_values, 0, name_title_group, nil)
104
+ else
105
+ write_multiple_parallel_contributors(parallel_name_values, contrib_name)
106
+ end
107
+ end
108
+
109
+ def write_multiple_parallel_contributors(parallel_name_values, contrib_name)
110
+ altrepgroup_id = id_generator.next_altrepgroup
111
+ parallel_name_values.each_with_index do |parallel_contrib_name, index|
112
+ display_name_present = parallel_name_values[index + 1].present? && parallel_name_values[index + 1].type == 'display'
113
+ Cocina::Models::Builders::NameTitleGroupBuilder.value_slices(parallel_contrib_name)&.each do |parallel_contrib_name_slice|
114
+ if name_title_vals_index[parallel_contrib_name_slice]
115
+ name_title_group = name_title_vals_index[parallel_contrib_name_slice]&.values&.first
116
+ if display_name_present
117
+ # associate type 'display' with the previous value
118
+ write_name_with_display_form(contributor, contrib_name, parallel_name_values, index, name_title_group, altrepgroup_id)
119
+ else
120
+ write_name_from_parallel(contributor, contrib_name, parallel_contrib_name, name_title_group, altrepgroup_id)
121
+ end
122
+ elsif display_name_present
123
+ # TODO: want a way to notify that we hit a problem - either notifier or HB error (issue #3751)
124
+ # OR validate for semantic correctness upon creation/update so we can't get here.
125
+ # notifier.warn("For contributor name '#{parallel_contrib_name_slice}', no contrib matching")
126
+ write_name_with_display_form(contributor, contrib_name, parallel_name_values, index, nil, altrepgroup_id)
127
+ elsif parallel_name_values[index].type == 'display'
128
+ # we assume we included this as part of a previous name
129
+ next
130
+ else
131
+ write_name_from_parallel(contributor, contrib_name, parallel_contrib_name, nil, altrepgroup_id)
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def write_name_from_parallel(contributor, name, parallel_name, name_title_group, altrepgroup_id)
109
138
  attributes = parallel_name_attributes(name, parallel_name, name_title_group, altrepgroup_id)
110
139
  type_attr = NAME_TYPE.fetch(contributor.type, name_title_group ? 'personal' : nil)
111
140
  attributes[:type] = type_attr if type_attr
@@ -121,6 +150,37 @@ module Cocina
121
150
  end
122
151
  end
123
152
 
153
+ # rubocop:disable Metrics/ParameterLists
154
+ def write_name_with_display_form(contributor, name, parallel_name_values, index, name_title_group, altrepgroup_id)
155
+ display_type_parallel_name = display_type_parallel_name(parallel_name_values)
156
+ parallel_name = parallel_name_values[index]
157
+ return if parallel_name.blank?
158
+
159
+ attributes = if altrepgroup_id.present?
160
+ parallel_name_attributes(name, parallel_name, name_title_group, altrepgroup_id)
161
+ else
162
+ name_attributes(contributor, name, nil)
163
+ end
164
+ type_attr = NAME_TYPE.fetch(contributor.type, name_title_group ? 'personal' : nil)
165
+ attributes[:type] = type_attr if type_attr
166
+ xml.name attributes do
167
+ if parallel_name.structuredValue.present?
168
+ write_structured(parallel_name)
169
+ else
170
+ write_basic(parallel_name)
171
+ end
172
+ write_display_form(display_type_parallel_name) if display_type_parallel_name.present?
173
+ write_identifier(contributor) if contributor.identifier.present?
174
+ write_note(contributor)
175
+ write_roles(contributor)
176
+ end
177
+ end
178
+ # rubocop:enable Metrics/ParameterLists
179
+
180
+ def display_type_parallel_name(parallel_name_values)
181
+ parallel_name_values.detect { |parallel_value| parallel_value.type == 'display' }
182
+ end
183
+
124
184
  def parallel_name_attributes(name, parallel_name, name_title_group, altrepgroup_id)
125
185
  {
126
186
  nameTitleGroup: name_title_group,
@@ -199,8 +259,6 @@ module Cocina
199
259
  xml.affiliation note.value
200
260
  when 'description'
201
261
  xml.description note.value
202
- when 'citation status'
203
- xml.description UNCITED_DESCRIPTION if note.value == 'false'
204
262
  end
205
263
  end
206
264
  end
@@ -394,7 +394,7 @@ module Cocina
394
394
  xml.name name_attrs do
395
395
  write_name_part(subject_value)
396
396
  write_display_form(display_values)
397
- write_roles(subject.note)
397
+ write_roles(subject_value.note)
398
398
  write_other_notes(subject.note, 'description')
399
399
  write_other_notes(subject.note, 'affiliation')
400
400
  end
@@ -410,7 +410,7 @@ module Cocina
410
410
  write_display_form(display_values)
411
411
  write_roles(subject.note)
412
412
  write_other_notes(subject.note, 'description')
413
- write_other_notes(subject.note, 'affiliation')
413
+ write_other_notes(subject_value.note, 'affiliation')
414
414
  end
415
415
  write_genres(subject_value)
416
416
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # The output of the message digest algorithm.
5
6
  class MessageDigest < Struct
6
7
  include Checkable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Metadata for a cocina object.
5
6
  class ObjectMetadata < Struct
6
7
  # When the object was created.
7
8
  attribute? :created, Types::Params::DateTime
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Presentation data for the File.
5
6
  class Presentation < Struct
6
7
  # Height in pixels
7
8
  attribute? :height, Types::Strict::Integer
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Other resource associated with the described resource.
5
6
  class RelatedResource < Struct
6
7
  # The relationship of the related resource to the described resource.
7
8
  attribute? :type, Types::Strict::String
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # A tag that indicates the item or collection should be released.
5
6
  class ReleaseTag < Struct
6
7
  # Who did this release
7
8
  # example: petucket
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Same as an AdminPolicy, but doesn't have an externalIdentifier as one will be created
5
6
  class RequestAdminPolicy < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Same as a Collection, but doesn't have an externalIdentifier as one will be created
5
6
  class RequestCollection < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Description that is included in a request to create a DRO. This is the same as a Description, except excludes PURL.
5
6
  class RequestDescription < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # A request to create a DRO. This has the same general structure as a DRO but doesn't have externalIdentifier and doesn't require the access subschema. If no access subschema is provided, these values will be inherited from the AdminPolicy.
5
6
  class RequestDRO < Struct
6
7
  include Validatable
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Structural metadata
5
6
  class RequestDROStructural < Struct
6
7
  attribute :contains, Types::Strict::Array.of(RequestFileSet).default([].freeze)
7
8
  attribute :hasMemberOrders, Types::Strict::Array.of(Sequence).default([].freeze)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Structural metadata
5
6
  class RequestFileSetStructural < Struct
6
7
  attribute :contains, Types::Strict::Array.of(RequestFile).default([].freeze)
7
8
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Same as a Identification, but requires a sourceId and doesn't permit a DOI.
5
6
  class RequestIdentification < Struct
6
7
  # A barcode
7
8
  attribute? :barcode, Types::Nominal::Any
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # A sequence or ordering of resources within a Collection or Object.
5
6
  class Sequence < Struct
6
7
  attribute :members, Types::Strict::Array.of(Types::Strict::String).default([].freeze)
7
8
  # The direction that a sequence of canvases should be displayed to the user
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Property model for indicating the vocabulary, authority, or other origin for a term, code, or identifier.
5
6
  class Source < Struct
6
7
  # Code representing the value source.
7
8
  attribute? :code, Types::Strict::String
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
+ # Property model for indicating the encoding, standard, or syntax to which a value conforms (e.g. RDA).
5
6
  class Standard < Struct
6
7
  # Code representing the standard or encoding.
7
8
  attribute? :code, Types::Strict::String
@@ -11,7 +11,7 @@ module Cocina
11
11
 
12
12
  def initialize(clazz, attributes)
13
13
  @clazz = clazz
14
- @attributes = attributes.deep_symbolize_keys
14
+ @attributes = attributes
15
15
  @error_paths = []
16
16
  end
17
17
 
@@ -29,9 +29,7 @@ module Cocina
29
29
  attr_reader :clazz, :attributes, :error_paths
30
30
 
31
31
  def meets_preconditions?
32
- resources.any? do |resource|
33
- titles_with_associated_name_note_for(resource).present?
34
- end
32
+ [Cocina::Models::Description, Cocina::Models::RequestDescription].include?(clazz)
35
33
  end
36
34
 
37
35
  def valid?(resource)
@@ -11,7 +11,7 @@ module Cocina
11
11
 
12
12
  def initialize(clazz, attributes)
13
13
  @clazz = clazz
14
- @attributes = attributes.deep_symbolize_keys
14
+ @attributes = attributes
15
15
  @error_paths = []
16
16
  end
17
17
 
@@ -30,8 +30,7 @@ module Cocina
30
30
  attr_reader :clazz, :attributes, :error_paths
31
31
 
32
32
  def meets_preconditions?
33
- attributes.key?(:description) || [Cocina::Models::Description,
34
- Cocina::Models::RequestDescription].include?(clazz)
33
+ [Cocina::Models::Description, Cocina::Models::RequestDescription].include?(clazz)
35
34
  end
36
35
 
37
36
  def validate_hash(hash, path)
@@ -11,7 +11,7 @@ module Cocina
11
11
 
12
12
  def initialize(clazz, attributes)
13
13
  @clazz = clazz
14
- @attributes = attributes.deep_symbolize_keys
14
+ @attributes = attributes
15
15
  @error_paths = []
16
16
  end
17
17
 
@@ -30,8 +30,7 @@ module Cocina
30
30
  attr_reader :clazz, :attributes, :error_paths
31
31
 
32
32
  def meets_preconditions?
33
- attributes.key?(:description) || [Cocina::Models::Description,
34
- Cocina::Models::RequestDescription].include?(clazz)
33
+ [Cocina::Models::Description, Cocina::Models::RequestDescription].include?(clazz)
35
34
  end
36
35
 
37
36
  def validate_hash(hash, path)
@@ -26,8 +26,7 @@ module Cocina
26
26
  # In the meantime, copying code.
27
27
  attributes_hash = deep_transform_values(attributes.to_h) do |value|
28
28
  value.class.name.starts_with?('Cocina::Models') ? value.to_h : value
29
- end.with_indifferent_access
30
-
29
+ end.deep_symbolize_keys.with_indifferent_access
31
30
  VALIDATORS.each { |validator| validator.validate(clazz, attributes_hash) }
32
31
  end
33
32
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
- VERSION = '0.78.0'
5
+ VERSION = '0.81.0'
6
6
  end
7
7
  end
data/lib/cocina/models.rb CHANGED
@@ -119,7 +119,7 @@ module Cocina
119
119
  # @param [DROWithMetadata,CollectionWithMetadata,AdminPolicyWithMetadata] cocina_object
120
120
  # @return [DRO,Collection,AdminPolicy]
121
121
  def self.without_metadata(cocina_object)
122
- build(cocina_object.to_h.except(:created, :modified, :lock))
122
+ build(cocina_object.to_h.except(:created, :modified, :lock), validate: false)
123
123
  end
124
124
 
125
125
  # Adds metadata to a DRO, Collection, AdminPolicy
@@ -144,7 +144,7 @@ module Cocina
144
144
  else
145
145
  AdminPolicyWithMetadata
146
146
  end
147
- clazz.new(props)
147
+ clazz.new(props, false, false)
148
148
  end
149
149
 
150
150
  def self.type_for(dyn)
@@ -45,24 +45,31 @@ module Cocina
45
45
  type: Cocina::Models::ObjectType.object,
46
46
  id: 'druid:bc234fg5678',
47
47
  version: 1,
48
- label: 'test object',
49
- title: 'test object',
48
+ label: 'factory DRO label',
49
+ title: 'factory DRO title',
50
50
  source_id: 'sul:1234',
51
51
  admin_policy_id: 'druid:hv992ry2431'
52
52
  }.freeze
53
53
 
54
54
  REQUEST_DRO_DEFAULTS = DRO_DEFAULTS.except(:id)
55
55
 
56
- COLLECTION_DEFAULTS = DRO_DEFAULTS.except(:source_id).merge(type: Cocina::Models::ObjectType.collection)
56
+ COLLECTION_DEFAULTS = {
57
+ type: Cocina::Models::ObjectType.collection,
58
+ id: 'druid:bb222ff5555',
59
+ version: 1,
60
+ label: 'factory collection label',
61
+ title: 'factory collection title',
62
+ admin_policy_id: 'druid:hv992ry2431'
63
+ }.freeze
57
64
 
58
65
  REQUEST_COLLECTION_DEFAULTS = COLLECTION_DEFAULTS.except(:id)
59
66
 
60
67
  ADMIN_POLICY_DEFAULTS = {
61
68
  type: Cocina::Models::ObjectType.admin_policy,
62
- id: 'druid:bc234fg5678',
69
+ id: 'druid:cb432gf8765',
63
70
  version: 1,
64
- label: 'test admin policy',
65
- title: 'test admin policy',
71
+ label: 'factory APO label',
72
+ title: 'factory APO title',
66
73
  admin_policy_id: 'druid:hv992ry2431',
67
74
  agreement_id: 'druid:hp308wm0436'
68
75
  }.freeze
data/openapi.yml CHANGED
@@ -779,6 +779,7 @@ components:
779
779
  $ref: "#/components/schemas/DescriptiveValue"
780
780
  DescriptiveParallelContributor:
781
781
  description: Value model for multiple representations of information about the same contributor (e.g. in different languages).
782
+ deprecated: true
782
783
  type: object
783
784
  additionalProperties: false
784
785
  properties:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocina-models
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.78.0
4
+ version: 0.81.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-04 00:00:00.000000000 Z
11
+ date: 2022-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport