cocina-models 0.79.0 → 0.82.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +6 -5
  3. data/cocina-models.gemspec +3 -0
  4. data/docs/description_types.md +1 -0
  5. data/lib/cocina/generator/schema.rb +2 -2
  6. data/lib/cocina/generator/schema_base.rb +10 -0
  7. data/lib/cocina/generator/schema_value.rb +0 -4
  8. data/lib/cocina/models/access_role.rb +1 -0
  9. data/lib/cocina/models/access_role_member.rb +1 -0
  10. data/lib/cocina/models/admin_policy_access_template.rb +1 -0
  11. data/lib/cocina/models/admin_policy_administrative.rb +1 -0
  12. data/lib/cocina/models/admin_policy_with_metadata.rb +1 -0
  13. data/lib/cocina/models/applies_to.rb +1 -0
  14. data/lib/cocina/models/builders/name_title_group_builder.rb +0 -1
  15. data/lib/cocina/models/collection.rb +1 -0
  16. data/lib/cocina/models/collection_access.rb +1 -0
  17. data/lib/cocina/models/collection_with_metadata.rb +1 -0
  18. data/lib/cocina/models/contributor.rb +1 -0
  19. data/lib/cocina/models/descriptive_access_metadata.rb +1 -0
  20. data/lib/cocina/models/descriptive_admin_metadata.rb +1 -0
  21. data/lib/cocina/models/descriptive_basic_value.rb +1 -0
  22. data/lib/cocina/models/descriptive_geographic_metadata.rb +1 -0
  23. data/lib/cocina/models/descriptive_grouped_value.rb +1 -0
  24. data/lib/cocina/models/descriptive_parallel_contributor.rb +2 -0
  25. data/lib/cocina/models/descriptive_parallel_event.rb +1 -0
  26. data/lib/cocina/models/descriptive_parallel_value.rb +1 -0
  27. data/lib/cocina/models/descriptive_structured_value.rb +1 -0
  28. data/lib/cocina/models/descriptive_value.rb +1 -0
  29. data/lib/cocina/models/descriptive_value_language.rb +1 -0
  30. data/lib/cocina/models/dro.rb +1 -0
  31. data/lib/cocina/models/dro_structural.rb +1 -0
  32. data/lib/cocina/models/dro_with_metadata.rb +1 -0
  33. data/lib/cocina/models/event.rb +1 -0
  34. data/lib/cocina/models/file.rb +1 -0
  35. data/lib/cocina/models/file_access.rb +1 -0
  36. data/lib/cocina/models/file_set.rb +1 -0
  37. data/lib/cocina/models/file_set_structural.rb +1 -0
  38. data/lib/cocina/models/geographic.rb +1 -0
  39. data/lib/cocina/models/language.rb +1 -0
  40. data/lib/cocina/models/mapping/escape_html.rb +23 -0
  41. data/lib/cocina/models/mapping/from_mods/name_builder.rb +79 -23
  42. data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +27 -2
  43. data/lib/cocina/models/mapping/to_mods/name_writer.rb +79 -19
  44. data/lib/cocina/models/mapping/to_mods/note.rb +3 -1
  45. data/lib/cocina/models/mapping/to_mods/subject.rb +2 -2
  46. data/lib/cocina/models/message_digest.rb +1 -0
  47. data/lib/cocina/models/object_metadata.rb +1 -0
  48. data/lib/cocina/models/presentation.rb +1 -0
  49. data/lib/cocina/models/related_resource.rb +1 -0
  50. data/lib/cocina/models/release_tag.rb +1 -0
  51. data/lib/cocina/models/request_admin_policy.rb +1 -0
  52. data/lib/cocina/models/request_collection.rb +1 -0
  53. data/lib/cocina/models/request_description.rb +1 -0
  54. data/lib/cocina/models/request_dro.rb +1 -0
  55. data/lib/cocina/models/request_dro_structural.rb +1 -0
  56. data/lib/cocina/models/request_file_set_structural.rb +1 -0
  57. data/lib/cocina/models/request_identification.rb +1 -0
  58. data/lib/cocina/models/sequence.rb +1 -0
  59. data/lib/cocina/models/source.rb +1 -0
  60. data/lib/cocina/models/standard.rb +1 -0
  61. data/lib/cocina/models/validators/date_time_validator.rb +102 -0
  62. data/lib/cocina/models/version.rb +1 -1
  63. data/openapi.yml +1 -0
  64. metadata +47 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 193f5886756d2fd6432e90ab5323f6ed5a11c699cb97f49d61394bc5fafd5f53
4
- data.tar.gz: ecb1493a56b7b20f9733084c8a9a115e3a3c6639a8072bc371f53a51d69d0a2c
3
+ metadata.gz: ebc24ff8dbe8eb1fe740eb977abb73abc504409aa510905395e6660adcd05b3b
4
+ data.tar.gz: 62ad4e7cafce859250323e92cc2173257957b16775e767224a4de2e9fd639bae
5
5
  SHA512:
6
- metadata.gz: 8cc210b897872f3b90fd59e12f381f049b1389b0cf3d798a0375eea57282cf456c6591a8d474c5c612253123bca01ed9a9b931129c89153b8a8dae3ad713254b
7
- data.tar.gz: f1a23590223b7760780e3b547b053355a1ba600fdc468ce563aef20a4c38cd3571345bde621f3f9f23248d27c29f0f3b5a829dd0c7d5c08d95d27efec472453d
6
+ metadata.gz: 7a3b0ed8ab383f945450d42343dbaed6e709854ff0f3deca1f62b93c79883d3157b42f14cdaafd9392ef858620a0c58ae6867c16037c627499efddf3cb33880b
7
+ data.tar.gz: 9e1084ea73c6f547eab7b8a076d9e2a61a46361c9da1541effca8529d48d032621b6cd5868bd2886308aea1f834b4c586ae1058ca6f67f0315393e4eb632bbb9
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config --auto-gen-only-exclude`
3
- # on 2022-04-27 19:20:35 UTC using RuboCop version 1.28.2.
3
+ # on 2022-06-08 23:20:02 UTC using RuboCop version 1.28.2.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -35,7 +35,7 @@ Lint/UnusedMethodArgument:
35
35
  - 'lib/cocina/models/mapping/from_mods/subject.rb'
36
36
  - 'lib/cocina/models/mapping/to_mods/event.rb'
37
37
 
38
- # Offense count: 95
38
+ # Offense count: 96
39
39
  # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
40
40
  Metrics/AbcSize:
41
41
  Max: 40
@@ -56,16 +56,17 @@ Metrics/ParameterLists:
56
56
  RSpec/DescribeClass:
57
57
  Enabled: false
58
58
 
59
- # Offense count: 89
59
+ # Offense count: 87
60
60
  # Configuration parameters: CountAsOne.
61
61
  RSpec/ExampleLength:
62
62
  Max: 128
63
63
 
64
- # Offense count: 9
64
+ # Offense count: 10
65
65
  # Configuration parameters: Max.
66
66
  RSpec/NestedGroups:
67
67
  Exclude:
68
68
  - 'spec/cocina/models/mapping/normalizers/mods/origin_info_normalizer_spec.rb'
69
+ - 'spec/cocina/models/validators/date_time_validator_spec.rb'
69
70
 
70
71
  # Offense count: 1
71
72
  # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
@@ -87,7 +88,7 @@ Style/MultilineBlockChain:
87
88
  - 'lib/cocina/models/mapping/to_mods/form.rb'
88
89
  - 'lib/cocina/models/mapping/to_mods/subject.rb'
89
90
 
90
- # Offense count: 206
91
+ # Offense count: 223
91
92
  # This cop supports safe auto-correction (--auto-correct).
92
93
  # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
93
94
  # URISchemes: http, https
@@ -28,12 +28,15 @@ Gem::Specification.new do |spec|
28
28
  spec.add_dependency 'deprecation'
29
29
  spec.add_dependency 'dry-struct', '~> 1.0'
30
30
  spec.add_dependency 'dry-types', '~> 1.1'
31
+ spec.add_dependency 'edtf' # used for date/time validation
31
32
  spec.add_dependency 'equivalent-xml' # for diffing MODS
33
+ spec.add_dependency 'jsonpath' # used for date/time validation
32
34
  spec.add_dependency 'nokogiri'
33
35
  spec.add_dependency 'openapi3_parser' # Parsing openapi doc
34
36
  # Match these version requirements to what committee wants,
35
37
  # so that our client (non-committee) users have the same dependencies.
36
38
  spec.add_dependency 'openapi_parser', '>= 0.11.1', '< 1.0'
39
+ spec.add_dependency 'rss' # used for date/time validation
37
40
  spec.add_dependency 'super_diff'
38
41
  spec.add_dependency 'thor'
39
42
  spec.add_dependency 'zeitwerk', '~> 2.1'
@@ -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.
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ # Escaps HTML entities as CDATA for MODS since HTML is not permitted in MODS
7
+ class EscapeHtml
8
+ # @param [String] data
9
+ # @param [Nokogiri::XML::Builder]
10
+ def self.with_cdata(data, builder)
11
+ tokens = data.split(%r{(</?(?:i|cite)>)})
12
+ tokens.map do |token|
13
+ if /\A<.+>\z/.match? token
14
+ builder.cdata(token)
15
+ else
16
+ builder.text(token)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -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,
@@ -68,7 +68,9 @@ module Cocina
68
68
  else
69
69
  note.value
70
70
  end
71
- xml.public_send tag_name, value, attributes
71
+ xml.public_send tag_name, attributes do |builder|
72
+ EscapeHtml.with_cdata(value, builder) if value
73
+ end
72
74
  end
73
75
 
74
76
  def write_basic(note)
@@ -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
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'edtf'
4
+ require 'jsonpath'
5
+ require 'rss'
6
+
7
+ module Cocina
8
+ module Models
9
+ module Validators
10
+ # Validates that dates of known types are type-valid
11
+ class DateTimeValidator
12
+ VALIDATABLE_TYPES = %w[edtf iso8601 w3cdtf].freeze
13
+
14
+ def self.validate(_clazz, attributes)
15
+ new(attributes).validate
16
+ end
17
+
18
+ def initialize(attributes)
19
+ @attributes = attributes
20
+ end
21
+
22
+ def validate
23
+ return unless meets_preconditions?
24
+
25
+ return if invalid_dates.empty?
26
+
27
+ raise ValidationError, "Invalid date(s) for #{druid}: #{invalid_dates}"
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :attributes
33
+
34
+ def meets_preconditions?
35
+ attributes.key?(:description)
36
+ end
37
+
38
+ def invalid_dates
39
+ @invalid_dates ||= validatable_dates.filter_map do |date_hash|
40
+ code = date_hash.dig('encoding', 'code')
41
+ bad_values = JsonPath.new('$..value').on(date_hash.to_json).reject do |value|
42
+ send("valid_#{code}?", value)
43
+ end
44
+
45
+ next if bad_values.empty?
46
+
47
+ [*bad_values, code]
48
+ end
49
+ end
50
+
51
+ def validatable_dates
52
+ # Why is the `uniq` needed below? Odd behavior when handling highly nested use cases:
53
+ #
54
+ # > JsonPath.new("$..date..[?(@.encoding.code =~ /#{VALIDATABLE_TYPES.join('|')}/)]").on(attributes[:description].to_json)
55
+ # > [
56
+ # {"structuredValue"=>[{"value"=>"1996", "type"=>"start"}, {"value"=>"1998", "type"=>"end"}], "encoding"=>{"code"=>"iso8601"}},
57
+ # {"structuredValue"=>[{"value"=>"1996", "type"=>"start"}, {"value"=>"1998", "type"=>"end"}], "encoding"=>{"code"=>"iso8601"}}
58
+ # ]
59
+ #
60
+ # Notice how the JSONPath expression returns the *same exact* structure twice despite only being present once in the data.
61
+ JsonPath
62
+ .new("$..date..[?(@.encoding.code =~ /#{VALIDATABLE_TYPES.join('|')}/)]")
63
+ .on(attributes[:description].to_json)
64
+ .uniq
65
+ end
66
+
67
+ def valid_edtf?(value)
68
+ Date.edtf!(value)
69
+ true
70
+ rescue StandardError
71
+ false
72
+ end
73
+
74
+ def valid_iso8601?(value)
75
+ DateTime.iso8601(value)
76
+ true
77
+ rescue StandardError
78
+ false
79
+ end
80
+
81
+ def valid_w3cdtf?(value)
82
+ Time.w3cdtf(value)
83
+ true
84
+ rescue StandardError
85
+ # NOTE: the upstream W3CDTF implementation in the `rss` gem does not
86
+ # allow two patterns that should be valid per the specification:
87
+ #
88
+ # * YYYY
89
+ # * YYYY-MM
90
+ #
91
+ # So we catch the false positives from the upstream gem and allow
92
+ # these two patterns to validate
93
+ /\A\d{4}(-0[1-9]|1[0-2])?\Z/.match?(value)
94
+ end
95
+
96
+ def druid
97
+ @druid ||= attributes[:externalIdentifier]
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
- VERSION = '0.79.0'
5
+ VERSION = '0.82.0'
6
6
  end
7
7
  end
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.79.0
4
+ version: 0.82.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-09 00:00:00.000000000 Z
11
+ date: 2022-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: edtf
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: equivalent-xml
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: jsonpath
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: nokogiri
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -128,6 +156,20 @@ dependencies:
128
156
  - - "<"
129
157
  - !ruby/object:Gem::Version
130
158
  version: '1.0'
159
+ - !ruby/object:Gem::Dependency
160
+ name: rss
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :runtime
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
131
173
  - !ruby/object:Gem::Dependency
132
174
  name: super_diff
133
175
  requirement: !ruby/object:Gem::Requirement
@@ -377,6 +419,7 @@ files:
377
419
  - lib/cocina/models/location_based_access.rb
378
420
  - lib/cocina/models/location_based_download_access.rb
379
421
  - lib/cocina/models/mapping/error_notifier.rb
422
+ - lib/cocina/models/mapping/escape_html.rb
380
423
  - lib/cocina/models/mapping/from_mods/access.rb
381
424
  - lib/cocina/models/mapping/from_mods/admin_metadata.rb
382
425
  - lib/cocina/models/mapping/from_mods/alt_rep_group.rb
@@ -461,6 +504,7 @@ files:
461
504
  - lib/cocina/models/validators/associated_name_validator.rb
462
505
  - lib/cocina/models/validators/catalog_links_validator.rb
463
506
  - lib/cocina/models/validators/dark_validator.rb
507
+ - lib/cocina/models/validators/date_time_validator.rb
464
508
  - lib/cocina/models/validators/description_types_validator.rb
465
509
  - lib/cocina/models/validators/description_values_validator.rb
466
510
  - lib/cocina/models/validators/open_api_validator.rb
@@ -492,7 +536,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
492
536
  - !ruby/object:Gem::Version
493
537
  version: '0'
494
538
  requirements: []
495
- rubygems_version: 3.3.9
539
+ rubygems_version: 3.3.7
496
540
  signing_key:
497
541
  specification_version: 4
498
542
  summary: Data models for the SDR