cocina-models 0.119.0 → 0.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -1
  3. data/.rubocop.yml +6 -0
  4. data/Gemfile.lock +19 -28
  5. data/bin/validate-data +2 -0
  6. data/bin/validate-schema +6 -1
  7. data/cocina-models.gemspec +1 -2
  8. data/lib/cocina/models/mapping/from_mods/event.rb +12 -3
  9. data/lib/cocina/models/related_resource.rb +1 -1
  10. data/lib/cocina/models/validators/base_description_visitor_validator.rb +33 -0
  11. data/lib/cocina/models/validators/base_structural_visitor_validator.rb +23 -0
  12. data/lib/cocina/models/validators/composite_description_validator.rb +62 -0
  13. data/lib/cocina/models/validators/composite_structural_validator.rb +48 -0
  14. data/lib/cocina/models/validators/dark_visitor_validator.rb +46 -0
  15. data/lib/cocina/models/validators/description_date_time_visitor_validator.rb +132 -0
  16. data/lib/cocina/models/validators/{description_types_validator.rb → description_types_visitor_validator.rb} +8 -54
  17. data/lib/cocina/models/validators/{description_values_validator.rb → description_values_visitor_validator.rb} +14 -51
  18. data/lib/cocina/models/validators/json_schema_validator.rb +54 -102
  19. data/lib/cocina/models/validators/language_tag_visitor_validator.rb +32 -0
  20. data/lib/cocina/models/validators/reserved_filename_visitor_validator.rb +40 -0
  21. data/lib/cocina/models/validators/validator.rb +5 -9
  22. data/lib/cocina/models/version.rb +1 -1
  23. data/lib/cocina/models.rb +1 -1
  24. data/schema.json +114 -1
  25. metadata +13 -23
  26. data/lib/cocina/models/validators/dark_validator.rb +0 -76
  27. data/lib/cocina/models/validators/date_time_validator.rb +0 -100
  28. data/lib/cocina/models/validators/language_tag_validator.rb +0 -76
  29. data/lib/cocina/models/validators/reserved_filename_validator.rb +0 -60
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95fdb985d82245efee3898558df726439ee7d27e7fa45bd47815b44d52f58679
4
- data.tar.gz: 0a7644f423957828280316d0c5709847f260919e1ceb384c130e8196f4d5eb6a
3
+ metadata.gz: 36dff333c23753eed79b03fd9f4442339d677ba28baaf396408049bedd4ffc69
4
+ data.tar.gz: 3235a1280a037df4570e19de4f98f86577f6e048c7bb5b00d3c7f6a69dc2b377
5
5
  SHA512:
6
- metadata.gz: ce4a6079617e05fd8d5b1547d16e983e06f0f53914339f9f19dcb4a870d52c1c565844bb39b0ed5decffe2c68f2a4fc4f2b385b9d8dd4796700f2c1f9aed5c7b
7
- data.tar.gz: eb72b867dcab3ad0ee2e63d3b68276c17fb831365f4c6a785a95fa8250998b663c6bcca2d50d4ffc6786044a17df7321847c069595587a9ae3ac96120f093020
6
+ metadata.gz: 79466fddfa3fe42db002704a4d8f1e62e282e85dfe593aa7a6e58aea0449a247304d7b96596b15809f5a7f5ced5d0308160afee3efc95f3ec9051354e02e8eb2
7
+ data.tar.gz: b22313bbb8cea20829173cbf8569b5402472a7e01354a5e2790290771ecbb59e20eca361a94307b15fd2674f04ecfe62d7b57304eda8fb514bc0af7b35d9e832
data/.circleci/config.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  version: 2.1
2
2
  orbs:
3
- ruby-rails: sul-dlss/ruby-rails@4.8.0
3
+ ruby-rails: sul-dlss/ruby-rails@4.10.0
4
4
  workflows:
5
5
  build:
6
6
  jobs:
data/.rubocop.yml CHANGED
@@ -126,6 +126,12 @@ RSpec/BeEq: # new in 2.9.0
126
126
  RSpec/BeNil: # new in 2.9.0
127
127
  Enabled: true
128
128
 
129
+ RSpec/DiscardedMatcher: # new in 3.10
130
+ Enabled: true
131
+
132
+ RSpec/MatchWithSimpleRegex: # new in 3.10
133
+ Enabled: true
134
+
129
135
  RSpec/MultipleExpectations:
130
136
  Enabled: false
131
137
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cocina-models (0.119.0)
4
+ cocina-models (0.120.0)
5
5
  activesupport
6
6
  deprecation
7
7
  dry-struct (~> 1.0)
@@ -9,8 +9,7 @@ PATH
9
9
  edtf
10
10
  equivalent-xml
11
11
  i18n
12
- json_schemer (~> 2.0)
13
- jsonpath
12
+ jsonschema_rs
14
13
  nokogiri
15
14
  super_diff
16
15
  thor
@@ -74,7 +73,6 @@ GEM
74
73
  equivalent-xml (0.6.0)
75
74
  nokogiri (>= 1.4.3)
76
75
  erb (6.0.4)
77
- hana (1.3.7)
78
76
  i18n (1.14.8)
79
77
  concurrent-ruby (~> 1.0)
80
78
  ice_nine (0.11.2)
@@ -84,21 +82,17 @@ GEM
84
82
  prism (>= 1.3.0)
85
83
  rdoc (>= 4.0.0)
86
84
  reline (>= 0.4.2)
87
- json (2.19.7)
88
- json_schemer (2.5.0)
89
- bigdecimal
90
- hana (~> 1.3)
91
- regexp_parser (~> 2.0)
92
- simpleidn (~> 0.2)
93
- jsonpath (1.1.5)
94
- multi_json
85
+ json (2.19.8)
86
+ jsonschema_rs (0.46.5-arm64-darwin)
87
+ bigdecimal (>= 3.1, < 5)
88
+ jsonschema_rs (0.46.5-x86_64-linux)
89
+ bigdecimal (>= 3.1, < 5)
95
90
  language_server-protocol (3.17.0.5)
96
91
  lint_roller (1.1.0)
97
92
  logger (1.7.0)
98
93
  minitest (6.0.6)
99
94
  drb (~> 2.0)
100
95
  prism (~> 1.5)
101
- multi_json (1.21.1)
102
96
  nokogiri (1.19.3-arm64-darwin)
103
97
  racc (~> 1.4)
104
98
  nokogiri (1.19.3-x86_64-linux-gnu)
@@ -114,7 +108,7 @@ GEM
114
108
  prettyprint
115
109
  prettyprint (0.2.0)
116
110
  prism (1.9.0)
117
- psych (5.3.1)
111
+ psych (5.4.0)
118
112
  date
119
113
  stringio
120
114
  racc (1.8.1)
@@ -159,9 +153,10 @@ GEM
159
153
  rubocop-rake (0.7.1)
160
154
  lint_roller (~> 1.1)
161
155
  rubocop (>= 1.72.1)
162
- rubocop-rspec (3.9.0)
156
+ rubocop-rspec (3.10.2)
163
157
  lint_roller (~> 1.1)
164
- rubocop (~> 1.81)
158
+ regexp_parser (>= 2.0)
159
+ rubocop (~> 1.86, >= 1.86.2)
165
160
  ruby-progressbar (1.13.0)
166
161
  securerandom (0.4.1)
167
162
  simplecov (0.22.0)
@@ -170,7 +165,6 @@ GEM
170
165
  simplecov_json_formatter (~> 0.1)
171
166
  simplecov-html (0.13.2)
172
167
  simplecov_json_formatter (0.1.4)
173
- simpleidn (0.2.3)
174
168
  stringio (3.2.0)
175
169
  super_diff (0.19.0)
176
170
  attr_extras (>= 6.2.4, < 8)
@@ -209,8 +203,8 @@ CHECKSUMS
209
203
  attr_extras (7.1.0) sha256=d96fc9a9dd5d85ba2d37762440a816f840093959ae26bb90da994c2d9f1fc827
210
204
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
211
205
  bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
212
- bundler (4.0.12) sha256=7f8b757d28dfb636e7b24fba2344ac6dd13b5b24f4b46d62573d483f211825ac
213
- cocina-models (0.119.0)
206
+ bundler (4.0.13) sha256=19f08be7f27022cf0b89f27da0b044ae075e8270a9ef44ad248a932614e1ca3b
207
+ cocina-models (0.120.0)
214
208
  concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
215
209
  connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
216
210
  date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
@@ -227,19 +221,17 @@ CHECKSUMS
227
221
  edtf (3.2.0) sha256=a15a0ee274e49c8047a3ebb5d61d793ba44f7f8ffbf0595392c467e3ea8d2447
228
222
  equivalent-xml (0.6.0) sha256=8919761efa848ad0846369ff8be1f646b17e5061698c4867b09829000cc3f487
229
223
  erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
230
- hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
231
224
  i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
232
225
  ice_nine (0.11.2) sha256=5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db
233
226
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
234
227
  irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
235
- json (2.19.7) sha256=fe432c8639f6efff69f9d73b518a3705d9581ab93156f981ea72806e1e5bcc3e
236
- json_schemer (2.5.0) sha256=2f01fb4cce721a4e08dd068fc2030cffd0702a7f333f1ea2be6e8991f00ae396
237
- jsonpath (1.1.5) sha256=29f70467193a2dc93ab864ec3d3326d54267961acc623f487340eb9c34931dbe
228
+ json (2.19.8) sha256=6354310fd76ef69b87d5bd1f38b40d730613baf90b6803d2d0a48f618d32dfaa
229
+ jsonschema_rs (0.46.5-arm64-darwin) sha256=e80414ed67f0956d3e06474a2fa076fc4a7b722f00e5d7142b70289c016ac6f1
230
+ jsonschema_rs (0.46.5-x86_64-linux) sha256=345c65ec7a5abf8879b9c9356752f0fdf4c9926f6480458fc32803a871b5cbb3
238
231
  language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
239
232
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
240
233
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
241
234
  minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
242
- multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080
243
235
  nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42
244
236
  nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976
245
237
  optimist (3.2.1) sha256=8cf8a0fd69f3aa24ab48885d3a666717c27bc3d9edd6e976e18b9d771e72e34e
@@ -249,7 +241,7 @@ CHECKSUMS
249
241
  pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
250
242
  prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
251
243
  prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
252
- psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
244
+ psych (5.4.0) sha256=14f72d69a611af663d7d70e4a7b67d9eb1f3ae9f8d916b478961d5a0075ba5b7
253
245
  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
254
246
  rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
255
247
  rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
@@ -265,13 +257,12 @@ CHECKSUMS
265
257
  rubocop (1.87.0) sha256=b9d9ddf55116a513f8ef2c7ae660662d8b49301f118d3f0df61865b33a5c188d
266
258
  rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
267
259
  rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d
268
- rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
260
+ rubocop-rspec (3.10.2) sha256=0b3e2ecc592cd10ecbf0095bb58d1e357905276e069643523cc19eb7495f65e2
269
261
  ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
270
262
  securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
271
263
  simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
272
264
  simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
273
265
  simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
274
- simpleidn (0.2.3) sha256=08ce96f03fa1605286be22651ba0fc9c0b2d6272c9b27a260bc88be05b0d2c29
275
266
  stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
276
267
  super_diff (0.19.0) sha256=c35fc1c0daa223d67b203fe3fb49a6cfd67850a53920319565c3c654e03ec902
277
268
  thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
@@ -283,4 +274,4 @@ CHECKSUMS
283
274
  zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12
284
275
 
285
276
  BUNDLED WITH
286
- 4.0.12
277
+ 4.0.13
data/bin/validate-data CHANGED
@@ -191,6 +191,8 @@ def distribute_work(filename, workers, batch_size, total_lines) # rubocop:disabl
191
191
 
192
192
  # Update progress bar
193
193
  progressbar.increment
194
+
195
+ break if line_number >= total_lines
194
196
  end
195
197
  end
196
198
 
data/bin/validate-schema CHANGED
@@ -5,4 +5,9 @@ require 'bundler/setup'
5
5
  require 'cocina/models'
6
6
 
7
7
  filepath = ARGV[0]
8
- exit(1) unless JSONSchemer.valid_schema?(Pathname.new(filepath))
8
+ begin
9
+ JSONSchema.validator_for(JSON.parse(File.read(filepath)))
10
+ rescue StandardError => e
11
+ warn e.message
12
+ exit(1)
13
+ end
@@ -31,8 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency 'edtf' # used for date/time validation
32
32
  spec.add_dependency 'equivalent-xml' # for diffing MODS
33
33
  spec.add_dependency 'i18n' # for validating BCP 47 language tags, according to RFC 4646
34
- spec.add_dependency 'jsonpath' # used for date/time validation
35
- spec.add_dependency 'json_schemer', '~> 2.0'
34
+ spec.add_dependency 'jsonschema_rs'
36
35
  spec.add_dependency 'nokogiri'
37
36
  spec.add_dependency 'super_diff'
38
37
  spec.add_dependency 'thor'
@@ -435,7 +435,7 @@ module Cocina
435
435
  new_node = node.deep_dup
436
436
  new_node.remove_attribute('encoding') if common_attribs[:encoding].present? || node[:encoding]&.empty?
437
437
  new_node.remove_attribute('qualifier') if common_attribs[:qualifier].present? || node[:qualifier]&.empty?
438
- build_date(new_node)
438
+ build_date(new_node, encoding: common_attribs.dig(:encoding, :code))
439
439
  end
440
440
  { structuredValue: dates }.merge(common_attribs).compact
441
441
  end
@@ -462,9 +462,11 @@ module Cocina
462
462
  attribs.compact
463
463
  end
464
464
 
465
- def build_date(date_node)
465
+ def build_date(date_node, encoding: nil)
466
+ effective_encoding = date_node['encoding'] || encoding
466
467
  {}.tap do |date|
467
- date[:value] = clean_date(date_node.text) if date_node.text.present?
468
+ raw_value = clean_date(date_node.text)
469
+ date[:value] = effective_encoding == 'edtf' ? pad_edtf_year(raw_value) : raw_value if date_node.text.present?
468
470
  date[:encoding] = { code: date_node['encoding'] } if date_node['encoding']
469
471
  date[:status] = 'primary' if date_node['keyDate']
470
472
  date[:note] = build_date_note(date_node)
@@ -489,6 +491,13 @@ module Cocina
489
491
  date.delete_suffix('.')
490
492
  end
491
493
 
494
+ # Pads a 1-3 digit year to 4 digits, handling modifiers like ~,-.
495
+ def pad_edtf_year(value)
496
+ value.sub(%r{\A(-?)(\d{1,3})(?=[~?%/-]|\z)}) do
497
+ "#{::Regexp.last_match(1)}#{::Regexp.last_match(2).rjust(4, '0')}"
498
+ end
499
+ end
500
+
492
501
  # NOTE: Do any eventType/displayLabel transformations before determining role (i.e. with LEGACY_EVENT_TYPES_2_TYPE)
493
502
  def role_for(event)
494
503
  case event[:type]
@@ -14,7 +14,7 @@ module Cocina
14
14
  # The preferred display label to use for the related resource in access systems.
15
15
  attribute? :displayLabel, Types::Strict::String.optional
16
16
  # Titles of the related resource.
17
- attribute :title, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
17
+ attribute :title, Types::Strict::Array.of(Title).default([].freeze)
18
18
  # Agents contributing in some way to the creation and history of the related resource.
19
19
  attribute :contributor, Types::Strict::Array.of(Contributor).default([].freeze)
20
20
  # Events in the history of the related resource.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Super class for description validators that use a visitor pattern.
7
+ class BaseDescriptionVisitorValidator
8
+ def visit_hash(hash:, path:); end
9
+
10
+ def visit_array(array:, path:); end
11
+
12
+ def visit_obj(obj:, path:); end
13
+
14
+ # @raise [ValidationError] if validation fails
15
+ def validate!; end
16
+
17
+ def path_to_s(path)
18
+ # This matches the format used by descriptive spreadsheets
19
+ path_str = ''
20
+ path.each_with_index do |part, index|
21
+ if part.is_a?(Integer)
22
+ path_str += (part + 1).to_s
23
+ else
24
+ path_str += '.' if index.positive?
25
+ path_str += part.to_s
26
+ end
27
+ end
28
+ path_str
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Super class for structural validators that use a visitor pattern.
7
+ class BaseStructuralVisitorValidator
8
+ def initialize(attributes)
9
+ @attributes = attributes
10
+ end
11
+
12
+ def visit_file(file_hash:); end
13
+
14
+ # @raise [ValidationError] if validation fails
15
+ def validate!; end
16
+
17
+ private
18
+
19
+ attr_reader :attributes
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Composite validator for description that uses a visitor pattern to validate in a single pass.
7
+ class CompositeDescriptionValidator
8
+ VALIDATORS = [
9
+ DescriptionTypesVisitorValidator,
10
+ DescriptionValuesVisitorValidator,
11
+ DescriptionDateTimeVisitorValidator
12
+ ].freeze
13
+
14
+ def self.validate(clazz, attributes)
15
+ new(clazz, attributes).validate
16
+ end
17
+
18
+ def initialize(clazz, attributes, validators: VALIDATORS)
19
+ @clazz = clazz
20
+ @attributes = attributes
21
+ @validators = validators.map(&:new)
22
+ end
23
+
24
+ def validate
25
+ return unless meets_preconditions?
26
+
27
+ validate_obj(obj: attributes, path: [])
28
+
29
+ validators.each(&:validate!)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :clazz, :attributes, :validators
35
+
36
+ def meets_preconditions?
37
+ [Cocina::Models::Description, Cocina::Models::RequestDescription].include?(clazz)
38
+ end
39
+
40
+ def validate_hash(hash:, path:)
41
+ validators.each { |validator| validator.visit_hash(hash:, path:) }
42
+ hash.each do |key, obj|
43
+ validate_obj(obj:, path: path + [key])
44
+ end
45
+ end
46
+
47
+ def validate_array(array:, path:)
48
+ validators.each { |validator| validator.visit_array(array:, path:) }
49
+ array.each_with_index do |obj, index|
50
+ validate_obj(obj:, path: path + [index])
51
+ end
52
+ end
53
+
54
+ def validate_obj(obj:, path:)
55
+ validators.each { |validator| validator.visit_obj(obj:, path:) }
56
+ validate_hash(hash: obj, path: path) if obj.is_a?(Hash)
57
+ validate_array(array: obj, path: path) if obj.is_a?(Array)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Composite validator for structural metadata that uses a visitor pattern to validate files in a single pass.
7
+ class CompositeStructuralValidator
8
+ VALIDATORS = [
9
+ DarkVisitorValidator,
10
+ LanguageTagVisitorValidator,
11
+ ReservedFilenameVisitorValidator
12
+ ].freeze
13
+
14
+ def self.validate(clazz, attributes)
15
+ new(clazz, attributes).validate
16
+ end
17
+
18
+ def initialize(clazz, attributes, validators: VALIDATORS)
19
+ @clazz = clazz
20
+ @attributes = attributes
21
+ @validators = validators.map { |v| v.new(attributes) }
22
+ end
23
+
24
+ def validate
25
+ return unless meets_preconditions?
26
+
27
+ Array(attributes.dig(:structural, :contains)).each do |fileset_hash|
28
+ Array(fileset_hash.dig(:structural, :contains)).each do |file_hash|
29
+ validators.each { |validator| validator.visit_file(file_hash:) }
30
+ end
31
+ end
32
+
33
+ validators.each(&:validate!)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :clazz, :attributes, :validators
39
+
40
+ def meets_preconditions?
41
+ clazz::TYPES.intersect?(DRO::TYPES)
42
+ rescue NameError
43
+ false
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Validates that shelve and publish file attributes are set to false for dark DRO objects.
7
+ class DarkVisitorValidator < BaseStructuralVisitorValidator
8
+ def visit_file(file_hash:)
9
+ return unless dark_object?
10
+
11
+ invalid_files << file_hash if invalid?(file_hash)
12
+ end
13
+
14
+ def validate!
15
+ return if invalid_files.empty?
16
+
17
+ filenames = invalid_files.map { |file| file[:filename] || file[:label] }
18
+ raise ValidationError, 'Not all files have dark access and/or are unshelved ' \
19
+ "when object access is dark: #{filenames}"
20
+ end
21
+
22
+ private
23
+
24
+ def invalid_files
25
+ @invalid_files ||= []
26
+ end
27
+
28
+ def dark_object?
29
+ # Checking for nil to account for default being dark.
30
+ @dark_object ||= ['dark', nil].include?(attributes.dig(:access, :view))
31
+ end
32
+
33
+ def invalid?(file)
34
+ # Ignore if a WARC
35
+ return false if file[:hasMimeType] == 'application/warc'
36
+
37
+ return true if file.dig(:administrative, :shelve)
38
+ # Checking for nil to account for default being dark.
39
+ return true if ['dark', nil].exclude?(file.dig(:access, :view))
40
+
41
+ false
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'edtf'
4
+
5
+ module Cocina
6
+ module Models
7
+ module Validators
8
+ # Validates that dates of known types are type-valid using the visitor pattern.
9
+ class DescriptionDateTimeVisitorValidator < BaseDescriptionVisitorValidator
10
+ VALIDATABLE_TYPES = %w[edtf iso8601 w3cdtf].freeze
11
+
12
+ def visit_hash(hash:, path:) # rubocop:disable Metrics/CyclomaticComplexity
13
+ # Only dates nested under a `date` key are subject to validation.
14
+ # For example, event.date is in scope but event.note is not.
15
+ return unless in_date_path?(path)
16
+
17
+ # A hash with a validatable encoding.code "owns" the encoding for its
18
+ # entire subtree. For example, the outer hash below owns iso8601 for
19
+ # both structuredValue children even though those children carry no
20
+ # encoding themselves:
21
+ #
22
+ # date: [{
23
+ # structuredValue: [
24
+ # { value: '1996', type: 'start' },
25
+ # { value: '1998', type: 'end' }
26
+ # ],
27
+ # encoding: { code: 'iso8601' } # ← registered at path [:date, 0]
28
+ # }]
29
+ #
30
+ # We record the path before visiting children because
31
+ # CompositeDescriptionValidator calls visit_hash on a parent before
32
+ # recursing into its children, so the encoding is always registered
33
+ # before any child value hashes are visited.
34
+ code = hash.dig(:encoding, :code)
35
+ encoding_paths[path.dup] = code if code && VALIDATABLE_TYPES.include?(code)
36
+
37
+ value = hash[:value]
38
+ return unless value.is_a?(String)
39
+
40
+ # Resolve which encoding governs this value by finding the longest
41
+ # registered encoding path that is a prefix of the current path.
42
+ # Longest-prefix wins so that a more-specific inner encoding overrides
43
+ # a less-specific outer one. For example, given:
44
+ #
45
+ # date: [{
46
+ # parallelValue: [
47
+ # { value: '1996', encoding: { code: 'edtf' } }, # path [:date,0,:parallelValue,0]
48
+ # { value: '一九九六' } # path [:date,0,:parallelValue,1]
49
+ # ],
50
+ # encoding: { code: 'iso8601' } # path [:date,0]
51
+ # }]
52
+ #
53
+ # The value '1996' at [:date,0,:parallelValue,0] matches both [:date,0]
54
+ # (iso8601) and [:date,0,:parallelValue,0] (edtf); the longer prefix wins
55
+ # and it is validated as edtf. The value '一九九六' at
56
+ # [:date,0,:parallelValue,1] only matches [:date,0] (iso8601).
57
+ encoding_path, code = find_encoding_for(path)
58
+ return unless code
59
+
60
+ invalid_groups[encoding_path] ||= []
61
+ invalid_groups[encoding_path] << value unless valid_value?(value, code)
62
+ end
63
+
64
+ def validate!
65
+ return if invalid_groups.empty?
66
+
67
+ invalid_dates = invalid_groups.filter_map do |path, values|
68
+ next if values.empty?
69
+
70
+ [*values, encoding_paths[path]]
71
+ end
72
+
73
+ return if invalid_dates.empty?
74
+
75
+ raise ValidationError, "Invalid date(s) in description: #{invalid_dates}"
76
+ end
77
+
78
+ private
79
+
80
+ def encoding_paths
81
+ @encoding_paths ||= {}
82
+ end
83
+
84
+ def invalid_groups
85
+ @invalid_groups ||= {}
86
+ end
87
+
88
+ def in_date_path?(path)
89
+ path.any? { |part| part.to_s == 'date' }
90
+ end
91
+
92
+ def find_encoding_for(path)
93
+ encoding_paths
94
+ .select { |prefix, _| path.first(prefix.length) == prefix }
95
+ .max_by { |prefix, _| prefix.length }
96
+ end
97
+
98
+ def valid_value?(value, code)
99
+ send(:"valid_#{code}?", value)
100
+ end
101
+
102
+ def valid_edtf?(value)
103
+ return false if value == 'XXXX'
104
+
105
+ Date.edtf!(value)
106
+ true
107
+ rescue StandardError
108
+ # NOTE: the upstream EDTF implementation in the `edtf` gem does not
109
+ # allow a valid pattern that we use (possibly because only level
110
+ # 0 of the spec was implemented?):
111
+ #
112
+ # * Y-20555
113
+ #
114
+ # So we catch the false positives from the upstream gem and allow
115
+ # this pattern to validate
116
+ /\AY-?\d{5,}\Z/.match?(value)
117
+ end
118
+
119
+ def valid_iso8601?(value)
120
+ DateTime.iso8601(value)
121
+ true
122
+ rescue StandardError
123
+ false
124
+ end
125
+
126
+ def valid_w3cdtf?(value)
127
+ W3cdtfValidator.validate(value)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end