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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +1 -1
- data/.rubocop.yml +6 -0
- data/Gemfile.lock +19 -28
- data/bin/validate-data +2 -0
- data/bin/validate-schema +6 -1
- data/cocina-models.gemspec +1 -2
- data/lib/cocina/models/mapping/from_mods/event.rb +12 -3
- data/lib/cocina/models/related_resource.rb +1 -1
- data/lib/cocina/models/validators/base_description_visitor_validator.rb +33 -0
- data/lib/cocina/models/validators/base_structural_visitor_validator.rb +23 -0
- data/lib/cocina/models/validators/composite_description_validator.rb +62 -0
- data/lib/cocina/models/validators/composite_structural_validator.rb +48 -0
- data/lib/cocina/models/validators/dark_visitor_validator.rb +46 -0
- data/lib/cocina/models/validators/description_date_time_visitor_validator.rb +132 -0
- data/lib/cocina/models/validators/{description_types_validator.rb → description_types_visitor_validator.rb} +8 -54
- data/lib/cocina/models/validators/{description_values_validator.rb → description_values_visitor_validator.rb} +14 -51
- data/lib/cocina/models/validators/json_schema_validator.rb +54 -102
- data/lib/cocina/models/validators/language_tag_visitor_validator.rb +32 -0
- data/lib/cocina/models/validators/reserved_filename_visitor_validator.rb +40 -0
- data/lib/cocina/models/validators/validator.rb +5 -9
- data/lib/cocina/models/version.rb +1 -1
- data/lib/cocina/models.rb +1 -1
- data/schema.json +114 -1
- metadata +13 -23
- data/lib/cocina/models/validators/dark_validator.rb +0 -76
- data/lib/cocina/models/validators/date_time_validator.rb +0 -100
- data/lib/cocina/models/validators/language_tag_validator.rb +0 -76
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36dff333c23753eed79b03fd9f4442339d677ba28baaf396408049bedd4ffc69
|
|
4
|
+
data.tar.gz: 3235a1280a037df4570e19de4f98f86577f6e048c7bb5b00d3c7f6a69dc2b377
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79466fddfa3fe42db002704a4d8f1e62e282e85dfe593aa7a6e58aea0449a247304d7b96596b15809f5a7f5ced5d0308160afee3efc95f3ec9051354e02e8eb2
|
|
7
|
+
data.tar.gz: b22313bbb8cea20829173cbf8569b5402472a7e01354a5e2790290771ecbb59e20eca361a94307b15fd2674f04ecfe62d7b57304eda8fb514bc0af7b35d9e832
|
data/.circleci/config.yml
CHANGED
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.
|
|
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
|
-
|
|
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.
|
|
88
|
-
|
|
89
|
-
bigdecimal
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
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.
|
|
156
|
+
rubocop-rspec (3.10.2)
|
|
163
157
|
lint_roller (~> 1.1)
|
|
164
|
-
|
|
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.
|
|
213
|
-
cocina-models (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.
|
|
236
|
-
|
|
237
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
277
|
+
4.0.13
|
data/bin/validate-data
CHANGED
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
|
-
|
|
8
|
+
begin
|
|
9
|
+
JSONSchema.validator_for(JSON.parse(File.read(filepath)))
|
|
10
|
+
rescue StandardError => e
|
|
11
|
+
warn e.message
|
|
12
|
+
exit(1)
|
|
13
|
+
end
|
data/cocina-models.gemspec
CHANGED
|
@@ -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 '
|
|
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
|
-
|
|
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(
|
|
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
|