cocina-models 0.80.0 → 0.83.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c78843bdc4b3ba3ec0ea9e2e8fa569fe7979de0e5d1a52fc4923103ed20de36
4
- data.tar.gz: b3e317a0154e15973e9a0e57aab07d0d91702a97a31889963d4df2c26339a6d4
3
+ metadata.gz: eed81e24ae6c7f8a21f7dd55ac24df69a599dd69b4aae3de0e726962fdcd9318
4
+ data.tar.gz: a522660cd996da1a703f921940364ffd1690dea8c068dbdb0378f54da35a4597
5
5
  SHA512:
6
- metadata.gz: 2fa6986b659b6474ed9ac476237c6da8ff75487bb5c7ab61b33a0226af95c44b22f14210524f258bd75bf826ea141dcc3bb88ce31e2a85b6f4590ec1ea353eec
7
- data.tar.gz: e41c8c60d4eafe62b0f4025213f73aa7629284a0e5f73e93d7b356204d075635a3ea7da3af1aaf9557de487e1eee5236c3eec23a92a04d17fc71ecec14e50c48
6
+ metadata.gz: 17070357c2d42eee762d4234a8ad3233b1384442f077b7e45bd0039a905c9f9bf7e9db1a322003c7fa5bfc4a0135194fa05451524233f9ea070f28de2eba063c
7
+ data.tar.gz: 637b4bb37a7557abf2591dfaf348d758d89038075292cf0e7135d9fc49042e469b6ce533ea0b63c7428d4bebc60dba91807b97af3c9919b6fb378cad50ee77df
data/.gitignore CHANGED
@@ -9,5 +9,3 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
-
13
- Gemfile.lock
data/.rubocop.yml CHANGED
@@ -184,7 +184,7 @@ Style/SoleNestedConditional: # (new in 0.89)
184
184
  Style/StringConcatenation:
185
185
  Enabled: true
186
186
 
187
- Gemspec/DateAssignment: # new in 1.10
187
+ Gemspec/DeprecatedAttributeAssignment: # new in 1.10
188
188
  Enabled: true
189
189
  Gemspec/RequireMFA: # new in 1.23
190
190
  Enabled: true
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
data/Gemfile.lock ADDED
@@ -0,0 +1,162 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cocina-models (0.83.0)
5
+ activesupport
6
+ deprecation
7
+ dry-struct (~> 1.0)
8
+ dry-types (~> 1.1)
9
+ edtf
10
+ equivalent-xml
11
+ jsonpath
12
+ nokogiri
13
+ openapi3_parser
14
+ openapi_parser (>= 0.11.1, < 1.0)
15
+ rss
16
+ super_diff
17
+ thor
18
+ zeitwerk (~> 2.1)
19
+
20
+ GEM
21
+ remote: https://rubygems.org/
22
+ specs:
23
+ activesupport (7.0.3)
24
+ concurrent-ruby (~> 1.0, >= 1.0.2)
25
+ i18n (>= 1.6, < 2)
26
+ minitest (>= 5.1)
27
+ tzinfo (~> 2.0)
28
+ ast (2.4.2)
29
+ attr_extras (6.2.5)
30
+ byebug (11.1.3)
31
+ committee (4.4.0)
32
+ json_schema (~> 0.14, >= 0.14.3)
33
+ openapi_parser (>= 0.11.1, < 1.0)
34
+ rack (>= 1.5)
35
+ commonmarker (0.23.5)
36
+ concurrent-ruby (1.1.10)
37
+ deprecation (1.1.0)
38
+ activesupport
39
+ diff-lcs (1.5.0)
40
+ docile (1.4.0)
41
+ dry-configurable (0.15.0)
42
+ concurrent-ruby (~> 1.0)
43
+ dry-core (~> 0.6)
44
+ dry-container (0.9.0)
45
+ concurrent-ruby (~> 1.0)
46
+ dry-configurable (~> 0.13, >= 0.13.0)
47
+ dry-core (0.7.1)
48
+ concurrent-ruby (~> 1.0)
49
+ dry-inflector (0.2.1)
50
+ dry-logic (1.2.0)
51
+ concurrent-ruby (~> 1.0)
52
+ dry-core (~> 0.5, >= 0.5)
53
+ dry-struct (1.4.0)
54
+ dry-core (~> 0.5, >= 0.5)
55
+ dry-types (~> 1.5)
56
+ ice_nine (~> 0.11)
57
+ dry-types (1.5.1)
58
+ concurrent-ruby (~> 1.0)
59
+ dry-container (~> 0.3)
60
+ dry-core (~> 0.5, >= 0.5)
61
+ dry-inflector (~> 0.1, >= 0.1.2)
62
+ dry-logic (~> 1.0, >= 1.0.2)
63
+ edtf (3.0.8)
64
+ activesupport (>= 3.0, < 8.0)
65
+ equivalent-xml (0.6.0)
66
+ nokogiri (>= 1.4.3)
67
+ i18n (1.10.0)
68
+ concurrent-ruby (~> 1.0)
69
+ ice_nine (0.11.2)
70
+ json (2.6.2)
71
+ json_schema (0.21.0)
72
+ jsonpath (1.1.2)
73
+ multi_json
74
+ mini_portile2 (2.8.0)
75
+ minitest (5.16.2)
76
+ multi_json (1.15.0)
77
+ nokogiri (1.13.6)
78
+ mini_portile2 (~> 2.8.0)
79
+ racc (~> 1.4)
80
+ openapi3_parser (0.9.2)
81
+ commonmarker (~> 0.17)
82
+ openapi_parser (0.15.0)
83
+ optimist (3.0.1)
84
+ parallel (1.22.1)
85
+ parser (3.1.2.0)
86
+ ast (~> 2.4.1)
87
+ patience_diff (1.2.0)
88
+ optimist (~> 3.0)
89
+ racc (1.6.0)
90
+ rack (2.2.4)
91
+ rainbow (3.1.1)
92
+ rake (13.0.6)
93
+ regexp_parser (2.5.0)
94
+ rexml (3.2.5)
95
+ rspec (3.11.0)
96
+ rspec-core (~> 3.11.0)
97
+ rspec-expectations (~> 3.11.0)
98
+ rspec-mocks (~> 3.11.0)
99
+ rspec-core (3.11.0)
100
+ rspec-support (~> 3.11.0)
101
+ rspec-expectations (3.11.0)
102
+ diff-lcs (>= 1.2.0, < 2.0)
103
+ rspec-support (~> 3.11.0)
104
+ rspec-mocks (3.11.1)
105
+ diff-lcs (>= 1.2.0, < 2.0)
106
+ rspec-support (~> 3.11.0)
107
+ rspec-support (3.11.0)
108
+ rspec_junit_formatter (0.5.1)
109
+ rspec-core (>= 2, < 4, != 2.12.0)
110
+ rss (0.2.9)
111
+ rexml
112
+ rubocop (1.31.1)
113
+ json (~> 2.3)
114
+ parallel (~> 1.10)
115
+ parser (>= 3.1.0.0)
116
+ rainbow (>= 2.2.2, < 4.0)
117
+ regexp_parser (>= 1.8, < 3.0)
118
+ rexml (>= 3.2.5, < 4.0)
119
+ rubocop-ast (>= 1.18.0, < 2.0)
120
+ ruby-progressbar (~> 1.7)
121
+ unicode-display_width (>= 1.4.0, < 3.0)
122
+ rubocop-ast (1.18.0)
123
+ parser (>= 3.1.1.0)
124
+ rubocop-rake (0.6.0)
125
+ rubocop (~> 1.0)
126
+ rubocop-rspec (2.12.1)
127
+ rubocop (~> 1.31)
128
+ ruby-progressbar (1.11.0)
129
+ simplecov (0.21.2)
130
+ docile (~> 1.1)
131
+ simplecov-html (~> 0.11)
132
+ simplecov_json_formatter (~> 0.1)
133
+ simplecov-html (0.12.3)
134
+ simplecov_json_formatter (0.1.4)
135
+ super_diff (0.9.0)
136
+ attr_extras (>= 6.2.4)
137
+ diff-lcs
138
+ patience_diff
139
+ thor (1.2.1)
140
+ tzinfo (2.0.4)
141
+ concurrent-ruby (~> 1.0)
142
+ unicode-display_width (2.2.0)
143
+ zeitwerk (2.6.0)
144
+
145
+ PLATFORMS
146
+ ruby
147
+
148
+ DEPENDENCIES
149
+ bundler (~> 2.0)
150
+ byebug
151
+ cocina-models!
152
+ committee
153
+ rake (~> 13.0)
154
+ rspec (~> 3.0)
155
+ rspec_junit_formatter
156
+ rubocop (~> 1.24)
157
+ rubocop-rake
158
+ rubocop-rspec (~> 2.1)
159
+ simplecov
160
+
161
+ BUNDLED WITH
162
+ 2.3.17
data/README.md CHANGED
@@ -124,18 +124,9 @@ At the same, we have found it convenient to use these PRs to also bump the versi
124
124
 
125
125
  ### Step 4: Update other dependent applications
126
126
 
127
- Once the above listed steps have been completed, all the following applications that use cocina-models should be updated and released at the same time. "Cocina Level 2" describes this set of updates.
128
-
129
- * [sul-dlss/argo](https://github.com/sul-dlss/argo/)
130
- * [sul-dlss/common-accessioning](https://github.com/sul-dlss/common-accessioning/)
131
- * [sul-dlss/dor_indexing_app](https://github.com/sul-dlss/dor_indexing_app/)
132
- * [sul-dlss/google-books](https://github.com/sul-dlss/google-books/)
133
- * [sul-dlss/happy-heron](https://github.com/sul-dlss/happy-heron/)
134
- * [sul-dlss/hydra_etd](https://github.com/sul-dlss/hydra_etd/)
135
- * [sul-dlss/infrastructure-integration-test](https://github.com/sul-dlss/infrastructure-integration-test/)
136
- * [sul-dlss/pre-assembly](https://github.com/sul-dlss/pre-assembly/)
137
-
138
- There are scripts to help with this:
127
+ Once the above listed steps have been completed, all applications that use cocina-models should be updated and released at the same time. "Cocina Level 2" describes this set of updates. The applications that use cocina-models are those in [this list](https://github.com/sul-dlss/access-update-scripts/blob/master/infrastructure/projects.yml) that are NOT marked with `cocina_level2: false`.
128
+
129
+ There are scripts to help with updating other dependent applications:
139
130
 
140
131
  #### Step 4A: Create the PRs
141
132
 
@@ -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'
@@ -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
@@ -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,7 +36,7 @@ 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
@@ -52,7 +52,7 @@ module Cocina
52
52
  }.compact.merge(common_lang_script(name_node))
53
53
 
54
54
  name_attrs = name_attrs.merge(common_name(name_node, name_attrs[:name], is_parallel: true))
55
- name_parts = build_name_parts(name_node)
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
@@ -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')
@@ -70,12 +70,10 @@ module Cocina
70
70
  end
71
71
 
72
72
  def extract_type(geo)
73
- type = geo[:form].find do |form|
74
- form[:type].match(TYPE_REGEX) || (form[:type].match(MEDIA_REGEX) && form[:value] == 'Image')
73
+ type = geo.form.find do |form|
74
+ form.type.match?(TYPE_REGEX) || (form.type.match?(MEDIA_REGEX) && form.value == 'Image')
75
75
  end
76
- return type[:value] if type
77
-
78
- nil
76
+ type&.value
79
77
  end
80
78
 
81
79
  def about(druid)
@@ -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)
@@ -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.80.0'
5
+ VERSION = '0.83.0'
6
6
  end
7
7
  end
data/openapi.yml CHANGED
@@ -390,7 +390,7 @@ components:
390
390
  - catalogRecordId
391
391
  - refresh
392
392
  CatkeyBarcode:
393
- description: The barcode associated with a DRO object based on catkey, prefixed with 36105
393
+ description: The barcode associated with a DRO object based on catkey, prefixed with a catkey followed by a hyphen
394
394
  type: string
395
395
  pattern: '^[0-9]+-[0-9]+$'
396
396
  example: '6772719-1001'
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.80.0
4
+ version: 0.83.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-13 00:00:00.000000000 Z
11
+ date: 2022-07-05 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
@@ -297,6 +339,7 @@ files:
297
339
  - ".rubocop.yml"
298
340
  - ".rubocop_todo.yml"
299
341
  - Gemfile
342
+ - Gemfile.lock
300
343
  - README.md
301
344
  - Rakefile
302
345
  - bin/console
@@ -377,6 +420,7 @@ files:
377
420
  - lib/cocina/models/location_based_access.rb
378
421
  - lib/cocina/models/location_based_download_access.rb
379
422
  - lib/cocina/models/mapping/error_notifier.rb
423
+ - lib/cocina/models/mapping/escape_html.rb
380
424
  - lib/cocina/models/mapping/from_mods/access.rb
381
425
  - lib/cocina/models/mapping/from_mods/admin_metadata.rb
382
426
  - lib/cocina/models/mapping/from_mods/alt_rep_group.rb
@@ -461,6 +505,7 @@ files:
461
505
  - lib/cocina/models/validators/associated_name_validator.rb
462
506
  - lib/cocina/models/validators/catalog_links_validator.rb
463
507
  - lib/cocina/models/validators/dark_validator.rb
508
+ - lib/cocina/models/validators/date_time_validator.rb
464
509
  - lib/cocina/models/validators/description_types_validator.rb
465
510
  - lib/cocina/models/validators/description_values_validator.rb
466
511
  - lib/cocina/models/validators/open_api_validator.rb
@@ -492,7 +537,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
492
537
  - !ruby/object:Gem::Version
493
538
  version: '0'
494
539
  requirements: []
495
- rubygems_version: 3.3.9
540
+ rubygems_version: 3.3.7
496
541
  signing_key:
497
542
  specification_version: 4
498
543
  summary: Data models for the SDR