cocina-models 0.117.0 → 0.118.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/Gemfile.lock +23 -22
- data/README.md +5 -5
- data/lib/cocina/generator/generator.rb +3 -1
- data/lib/cocina/generator/schema.rb +23 -46
- data/lib/cocina/models/descriptive_value.rb +1 -1
- data/lib/cocina/models/dro_access.rb +11 -11
- data/lib/cocina/models/embargo.rb +9 -9
- data/lib/cocina/models/mapping/normalizers/mods_normalizer.rb +8 -7
- data/lib/cocina/models/title.rb +1 -1
- data/lib/cocina/models/validators/json_schema_validator.rb +149 -16
- data/lib/cocina/models/version.rb +1 -1
- data/schema.json +721 -285
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee0ad9dfc1b1845db07b8d94510214d98be20b7d732e0909dfe1935c046e9a34
|
|
4
|
+
data.tar.gz: 60f04cbd3e7531c45357c546d490851b8b781f0224468c2d2c81d9e3e550575f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70072f8f40350f2341e230f2cee198c72872b418eda5e1682e7ab1cdb7fac5c5121386a6e53d3d0c3995139b80797ec51783f0630a9844d948cc8fd79afa7f19
|
|
7
|
+
data.tar.gz: c56237be3e22069ade32d6ad81d9a989c78759dc9d3ff8a99150709958da55f942707670aa2b7aad05f35d9dcbc9e9d073a01f47079f8deefbb7d42902725a77
|
data/.circleci/config.yml
CHANGED
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.118.0)
|
|
5
5
|
activesupport
|
|
6
6
|
deprecation
|
|
7
7
|
dry-struct (~> 1.0)
|
|
@@ -84,7 +84,7 @@ GEM
|
|
|
84
84
|
prism (>= 1.3.0)
|
|
85
85
|
rdoc (>= 4.0.0)
|
|
86
86
|
reline (>= 0.4.2)
|
|
87
|
-
json (2.19.
|
|
87
|
+
json (2.19.5)
|
|
88
88
|
json_schemer (2.5.0)
|
|
89
89
|
bigdecimal
|
|
90
90
|
hana (~> 1.3)
|
|
@@ -95,13 +95,13 @@ GEM
|
|
|
95
95
|
language_server-protocol (3.17.0.5)
|
|
96
96
|
lint_roller (1.1.0)
|
|
97
97
|
logger (1.7.0)
|
|
98
|
-
minitest (6.0.
|
|
98
|
+
minitest (6.0.6)
|
|
99
99
|
drb (~> 2.0)
|
|
100
100
|
prism (~> 1.5)
|
|
101
|
-
multi_json (1.
|
|
102
|
-
nokogiri (1.19.
|
|
101
|
+
multi_json (1.21.1)
|
|
102
|
+
nokogiri (1.19.3-arm64-darwin)
|
|
103
103
|
racc (~> 1.4)
|
|
104
|
-
nokogiri (1.19.
|
|
104
|
+
nokogiri (1.19.3-x86_64-linux-gnu)
|
|
105
105
|
racc (~> 1.4)
|
|
106
106
|
optimist (3.2.1)
|
|
107
107
|
parallel (2.1.0)
|
|
@@ -142,7 +142,7 @@ GEM
|
|
|
142
142
|
rspec-support (3.13.7)
|
|
143
143
|
rspec_junit_formatter (0.6.0)
|
|
144
144
|
rspec-core (>= 2, < 4, != 2.12.0)
|
|
145
|
-
rubocop (1.86.
|
|
145
|
+
rubocop (1.86.2)
|
|
146
146
|
json (~> 2.3)
|
|
147
147
|
language_server-protocol (~> 3.17.0.2)
|
|
148
148
|
lint_roller (~> 1.1.0)
|
|
@@ -172,10 +172,10 @@ GEM
|
|
|
172
172
|
simplecov_json_formatter (0.1.4)
|
|
173
173
|
simpleidn (0.2.3)
|
|
174
174
|
stringio (3.2.0)
|
|
175
|
-
super_diff (0.
|
|
176
|
-
attr_extras (>= 6.2.4)
|
|
177
|
-
diff-lcs
|
|
178
|
-
patience_diff
|
|
175
|
+
super_diff (0.19.0)
|
|
176
|
+
attr_extras (>= 6.2.4, < 8)
|
|
177
|
+
diff-lcs (~> 1.5)
|
|
178
|
+
patience_diff (~> 1.2)
|
|
179
179
|
thor (1.5.0)
|
|
180
180
|
tsort (0.2.0)
|
|
181
181
|
tzinfo (2.0.6)
|
|
@@ -184,7 +184,7 @@ GEM
|
|
|
184
184
|
unicode-emoji (~> 4.1)
|
|
185
185
|
unicode-emoji (4.2.0)
|
|
186
186
|
uri (1.1.1)
|
|
187
|
-
zeitwerk (2.
|
|
187
|
+
zeitwerk (2.8.2)
|
|
188
188
|
|
|
189
189
|
PLATFORMS
|
|
190
190
|
arm64-darwin
|
|
@@ -209,7 +209,8 @@ CHECKSUMS
|
|
|
209
209
|
attr_extras (7.1.0) sha256=d96fc9a9dd5d85ba2d37762440a816f840093959ae26bb90da994c2d9f1fc827
|
|
210
210
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
211
211
|
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
212
|
-
|
|
212
|
+
bundler (4.0.12) sha256=7f8b757d28dfb636e7b24fba2344ac6dd13b5b24f4b46d62573d483f211825ac
|
|
213
|
+
cocina-models (0.118.0)
|
|
213
214
|
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
214
215
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
215
216
|
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
|
@@ -231,16 +232,16 @@ CHECKSUMS
|
|
|
231
232
|
ice_nine (0.11.2) sha256=5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db
|
|
232
233
|
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
|
233
234
|
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
|
|
234
|
-
json (2.19.
|
|
235
|
+
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
|
|
235
236
|
json_schemer (2.5.0) sha256=2f01fb4cce721a4e08dd068fc2030cffd0702a7f333f1ea2be6e8991f00ae396
|
|
236
237
|
jsonpath (1.1.5) sha256=29f70467193a2dc93ab864ec3d3326d54267961acc623f487340eb9c34931dbe
|
|
237
238
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
238
239
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
239
240
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
240
|
-
minitest (6.0.
|
|
241
|
-
multi_json (1.
|
|
242
|
-
nokogiri (1.19.
|
|
243
|
-
nokogiri (1.19.
|
|
241
|
+
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
|
242
|
+
multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080
|
|
243
|
+
nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42
|
|
244
|
+
nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976
|
|
244
245
|
optimist (3.2.1) sha256=8cf8a0fd69f3aa24ab48885d3a666717c27bc3d9edd6e976e18b9d771e72e34e
|
|
245
246
|
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
|
|
246
247
|
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
|
@@ -261,7 +262,7 @@ CHECKSUMS
|
|
|
261
262
|
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
|
|
262
263
|
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
|
|
263
264
|
rspec_junit_formatter (0.6.0) sha256=40dde674e6ae4e6cc0ff560da25497677e34fefd2338cc467a8972f602b62b15
|
|
264
|
-
rubocop (1.86.
|
|
265
|
+
rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d
|
|
265
266
|
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
|
266
267
|
rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d
|
|
267
268
|
rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
|
|
@@ -272,14 +273,14 @@ CHECKSUMS
|
|
|
272
273
|
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
|
273
274
|
simpleidn (0.2.3) sha256=08ce96f03fa1605286be22651ba0fc9c0b2d6272c9b27a260bc88be05b0d2c29
|
|
274
275
|
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
|
275
|
-
super_diff (0.
|
|
276
|
+
super_diff (0.19.0) sha256=c35fc1c0daa223d67b203fe3fb49a6cfd67850a53920319565c3c654e03ec902
|
|
276
277
|
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
|
277
278
|
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
278
279
|
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
|
|
279
280
|
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
280
281
|
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
281
282
|
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
|
|
282
|
-
zeitwerk (2.
|
|
283
|
+
zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12
|
|
283
284
|
|
|
284
285
|
BUNDLED WITH
|
|
285
|
-
4.0.
|
|
286
|
+
4.0.12
|
data/README.md
CHANGED
|
@@ -16,6 +16,10 @@ For more about the model for description see https://consul.stanford.edu/display
|
|
|
16
16
|
|
|
17
17
|
The [schema.json](schema.json) can also be viewed via JSON HERO: https://jsonhero.io/j/rynNH3NLBhcf and DOR Services App's openapi.yml: https://sul-dlss.github.io/dor-services-app/
|
|
18
18
|
|
|
19
|
+
### Schema Design
|
|
20
|
+
|
|
21
|
+
Our JSON Schema uses *terminal & mixin* composition strategy so we can enforce deep validation without running into **allOf** composition pitfalls. Terminal schemas (e.g., **DRO**, **Collection**, **AdminPolicy**, their **WithMetadata** variants, and **Request** variants) are the validation boundaries and generally set the `unevaluatedProperties: false` property. Reusable mixins (e.g., **DROMixin**, **CollectionMixin**, **AdminPolicyMixin**, and **AccessMixin**) are intentionally open and are composed into terminal schemas via **allOf** or **oneOf**, so these schemas **may not** take advantage of `unevaluatedProperties` to prevent unexpected properties.
|
|
22
|
+
|
|
19
23
|
## Configuration
|
|
20
24
|
|
|
21
25
|
Set the PURL url base:
|
|
@@ -121,9 +125,7 @@ Before you release a major or minor change, think about if this release will inc
|
|
|
121
125
|
If unsure, ask the team or ask for help to just run the validation report anyway (as described above).
|
|
122
126
|
|
|
123
127
|
### Partial release process
|
|
124
|
-
NOTE: If dependency updates are about to be released, you have the option of shortening the process and stopping after Step
|
|
125
|
-
|
|
126
|
-
IMPORTANT: If you do opt to skip steps 4 onward, you should NOT merge the dor-services-app and sdr-api PRs you created in step 3 until you are ready to finish the dependency updates process. You can have them reviewed and approved, but if you merge, you will greatly increase the risk of issues if the main branch of DSA or sdr-api are deployed after steps 1-3 are complete but before the rest of the apps are updated to use the new cocina-models via regular dependency updates. The fix for this is to either roll back DSA and sdr-api to the previous release tag, or proceed forwards with step 4-5.
|
|
128
|
+
NOTE: If dependency updates are about to be released, you have the option of shortening the process and stopping after Step 2. This is because Steps 3 onwards will be taken care of by the regular dependency updates process (basically the updating of cocina-models, dor-services-client and sdr-client as needed in the rest of the associated apps). You still do need to manually bump some gems and get those PRs approved and merged, as described in Steps 1-2 below.
|
|
127
129
|
|
|
128
130
|
### Step 0: Share intent to change the models
|
|
129
131
|
|
|
@@ -160,8 +162,6 @@ Perform `bundle update --conservative cocina-models dor-services-client` in the
|
|
|
160
162
|
|
|
161
163
|
Get the directly coupled services PRs merged before the deploy in step 5.
|
|
162
164
|
|
|
163
|
-
See the IMPORTANT note above about the timing of merging these PRs if you are waiting for dependency updates to make the updates to other dependent applications.
|
|
164
|
-
|
|
165
165
|
### Step 4: Update other dependent applications
|
|
166
166
|
|
|
167
167
|
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`.
|
|
@@ -102,8 +102,10 @@ module Cocina
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def schema_for(schema_name, lite: false)
|
|
105
|
+
return if schema_name.ends_with?('Mixin')
|
|
106
|
+
|
|
105
107
|
schema_doc = schemas[schema_name]
|
|
106
|
-
return
|
|
108
|
+
return if schema_doc.nil?
|
|
107
109
|
|
|
108
110
|
case schema_doc.type
|
|
109
111
|
when 'object'
|
|
@@ -5,21 +5,25 @@ module Cocina
|
|
|
5
5
|
# Class for generating from a JSON schema
|
|
6
6
|
class Schema < SchemaBase
|
|
7
7
|
def schema_properties
|
|
8
|
-
@schema_properties ||= (
|
|
8
|
+
@schema_properties ||= (
|
|
9
|
+
schema_properties_for(schema_doc) +
|
|
10
|
+
all_of_properties_for(schema_doc) +
|
|
11
|
+
one_of_properties_for(schema_doc)
|
|
12
|
+
).uniq(&:key)
|
|
9
13
|
end
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
VALIDATABLE_TYPES = %w[DRO RequestDRO DROWithMetadata Collection RequestCollection CollectionWithMetadata AdminPolicy
|
|
16
|
+
RequestAdminPolicy AdminPolicyWithMetadata Description RequestDescription].freeze
|
|
13
17
|
|
|
14
18
|
def generate
|
|
15
19
|
<<~RUBY
|
|
16
20
|
# frozen_string_literal: true
|
|
17
21
|
|
|
18
22
|
module Cocina
|
|
19
|
-
module Models
|
|
23
|
+
module Models
|
|
20
24
|
#{preamble}class #{name} < Struct
|
|
21
25
|
|
|
22
|
-
#{
|
|
26
|
+
#{validatable}
|
|
23
27
|
#{types}
|
|
24
28
|
|
|
25
29
|
#{model_attributes}
|
|
@@ -65,68 +69,41 @@ module Cocina
|
|
|
65
69
|
RUBY
|
|
66
70
|
end
|
|
67
71
|
|
|
68
|
-
def
|
|
69
|
-
return ''
|
|
72
|
+
def validatable
|
|
73
|
+
return '' if VALIDATABLE_TYPES.exclude?(name)
|
|
70
74
|
|
|
71
75
|
<<~RUBY
|
|
72
76
|
include Validatable
|
|
73
77
|
RUBY
|
|
74
78
|
end
|
|
75
79
|
|
|
76
|
-
def validatable?
|
|
77
|
-
VALIDATE_TYPES.include?(name) && !lite
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def properties
|
|
81
|
-
schema_properties_for(schema_doc)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def all_of_properties
|
|
85
|
-
all_of_properties_for(schema_doc)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def one_of_properties
|
|
89
|
-
one_of_properties_for(schema_doc)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
80
|
def all_of_properties_for(doc)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
doc.all_of.map do |all_of_schema|
|
|
96
|
-
# All of for this + recurse
|
|
81
|
+
Array(doc.all_of).flat_map do |all_of_schema|
|
|
97
82
|
schema_properties_for(all_of_schema) +
|
|
98
83
|
all_of_properties_for(all_of_schema) +
|
|
99
84
|
one_of_properties_for(all_of_schema)
|
|
100
|
-
end
|
|
85
|
+
end
|
|
101
86
|
end
|
|
102
87
|
|
|
103
88
|
def one_of_properties_for(doc)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
doc.one_of.flat_map do |one_of_doc|
|
|
110
|
-
one_of_doc.properties.map do |key, properties_doc|
|
|
111
|
-
property_class_for(properties_doc).new(properties_doc,
|
|
112
|
-
key: key,
|
|
113
|
-
# The property does less validation because may vary between
|
|
114
|
-
# different oneOf schemas. Validation is still performed
|
|
115
|
-
# by JSON Schema.
|
|
116
|
-
relaxed: true,
|
|
117
|
-
parent: self,
|
|
118
|
-
schemas: schemas)
|
|
119
|
-
end
|
|
89
|
+
Array(doc.one_of).flat_map do |one_of_schema|
|
|
90
|
+
schema_properties_for(one_of_schema, relaxed: true) +
|
|
91
|
+
all_of_properties_for(one_of_schema) +
|
|
92
|
+
one_of_properties_for(one_of_schema)
|
|
120
93
|
end
|
|
121
94
|
end
|
|
122
95
|
|
|
123
|
-
def schema_properties_for(doc)
|
|
96
|
+
def schema_properties_for(doc, relaxed: nil)
|
|
97
|
+
relax_all_properties = relaxed
|
|
98
|
+
|
|
124
99
|
Array(doc.properties).map do |key, properties_doc|
|
|
125
100
|
clazz = property_class_for(properties_doc)
|
|
101
|
+
relaxed = relax_all_properties.nil? ? lite && clazz != SchemaValue : relax_all_properties
|
|
102
|
+
|
|
126
103
|
clazz.new(properties_doc,
|
|
127
104
|
key: key,
|
|
128
105
|
required: doc.required&.include?(key),
|
|
129
|
-
relaxed:
|
|
106
|
+
relaxed: relaxed,
|
|
130
107
|
nullable: Array(properties_doc.type).include?('null'),
|
|
131
108
|
parent: self,
|
|
132
109
|
schemas: schemas)
|
|
@@ -4,6 +4,7 @@ module Cocina
|
|
|
4
4
|
module Models
|
|
5
5
|
# Default value model for descriptive elements.
|
|
6
6
|
class DescriptiveValue < Struct
|
|
7
|
+
attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
|
|
7
8
|
attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
8
9
|
attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
9
10
|
attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
@@ -40,7 +41,6 @@ module Cocina
|
|
|
40
41
|
attribute? :valueLanguage, DescriptiveValueLanguage.optional
|
|
41
42
|
# URL or other pointer to the location of the value of the descriptive element.
|
|
42
43
|
attribute? :valueAt, Types::Strict::String
|
|
43
|
-
attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -3,17 +3,6 @@
|
|
|
3
3
|
module Cocina
|
|
4
4
|
module Models
|
|
5
5
|
class DROAccess < Struct
|
|
6
|
-
# Access level.
|
|
7
|
-
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
8
|
-
attribute? :view, Types::Strict::String.optional.default('dark')
|
|
9
|
-
# Download access level.
|
|
10
|
-
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
11
|
-
attribute? :download, Types::Strict::String.optional.default('none')
|
|
12
|
-
# Not used for this access type, must be null.
|
|
13
|
-
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
14
|
-
attribute? :location, Types::Strict::String.optional
|
|
15
|
-
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
16
|
-
attribute? :controlledDigitalLending, Types::Strict::Bool.optional.default(false)
|
|
17
6
|
# The human readable copyright statement that applies
|
|
18
7
|
# example: Copyright World Trade Organization
|
|
19
8
|
attribute? :copyright, Copyright.optional
|
|
@@ -27,6 +16,17 @@ module Cocina
|
|
|
27
16
|
# The license governing reuse of the DRO. Should be an IRI for known licenses (i.e.
|
|
28
17
|
# CC, RightsStatement.org URI, etc.).
|
|
29
18
|
attribute? :license, License.optional.enum(nil, 'https://www.gnu.org/licenses/agpl.txt', 'https://www.apache.org/licenses/LICENSE-2.0', 'https://opensource.org/licenses/BSD-2-Clause', 'https://opensource.org/licenses/BSD-3-Clause', 'https://creativecommons.org/licenses/by/4.0/legalcode', 'https://creativecommons.org/licenses/by-nc/4.0/legalcode', 'https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode', 'https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode', 'https://creativecommons.org/licenses/by-nd/4.0/legalcode', 'https://creativecommons.org/licenses/by-sa/4.0/legalcode', 'https://creativecommons.org/publicdomain/zero/1.0/legalcode', 'https://opensource.org/licenses/cddl1', 'https://www.eclipse.org/legal/epl-2.0', 'https://www.gnu.org/licenses/gpl-3.0-standalone.html', 'https://www.isc.org/downloads/software-support-policy/isc-license/', 'https://www.gnu.org/licenses/lgpl-3.0-standalone.html', 'https://opensource.org/licenses/MIT', 'https://www.mozilla.org/MPL/2.0/', 'https://opendatacommons.org/licenses/by/1-0/', 'http://opendatacommons.org/licenses/odbl/1.0/', 'https://opendatacommons.org/licenses/odbl/1-0/', 'https://creativecommons.org/publicdomain/mark/1.0/', 'https://opendatacommons.org/licenses/pddl/1-0/', 'https://creativecommons.org/licenses/by/3.0/legalcode', 'https://creativecommons.org/licenses/by-sa/3.0/legalcode', 'https://creativecommons.org/licenses/by-nd/3.0/legalcode', 'https://creativecommons.org/licenses/by-nc/3.0/legalcode', 'https://creativecommons.org/licenses/by-nc-sa/3.0/legalcode', 'https://creativecommons.org/licenses/by-nc-nd/3.0/legalcode', 'https://cocina.sul.stanford.edu/licenses/none')
|
|
19
|
+
# Access level.
|
|
20
|
+
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
21
|
+
attribute? :view, Types::Strict::String.optional.default('dark')
|
|
22
|
+
# Download access level.
|
|
23
|
+
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
24
|
+
attribute? :download, Types::Strict::String.optional.default('none')
|
|
25
|
+
# Not used for this access type, must be null.
|
|
26
|
+
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
27
|
+
attribute? :location, Types::Strict::String.optional
|
|
28
|
+
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
29
|
+
attribute? :controlledDigitalLending, Types::Strict::Bool.optional.default(false)
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
module Cocina
|
|
4
4
|
module Models
|
|
5
5
|
class Embargo < Struct
|
|
6
|
+
# Date when the Collection is released from an embargo.
|
|
7
|
+
# example: 2029-06-22T07:00:00.000+00:00
|
|
8
|
+
attribute :releaseDate, Types::Params::DateTime
|
|
9
|
+
# The human readable use and reproduction statement that applies
|
|
10
|
+
# example: Property rights reside with the repository. Literary rights reside with
|
|
11
|
+
# the creators of the documents or their heirs. To obtain permission to publish or
|
|
12
|
+
# reproduce, please contact the Public Services Librarian of the Dept. of Special Collections
|
|
13
|
+
# (http://library.stanford.edu/spc).
|
|
14
|
+
attribute? :useAndReproductionStatement, UseAndReproductionStatement.optional
|
|
6
15
|
# Access level.
|
|
7
16
|
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
8
17
|
attribute? :view, Types::Strict::String.optional.default('dark')
|
|
@@ -14,15 +23,6 @@ module Cocina
|
|
|
14
23
|
attribute? :location, Types::Strict::String.optional
|
|
15
24
|
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
16
25
|
attribute? :controlledDigitalLending, Types::Strict::Bool.optional.default(false)
|
|
17
|
-
# Date when the Collection is released from an embargo.
|
|
18
|
-
# example: 2029-06-22T07:00:00.000+00:00
|
|
19
|
-
attribute :releaseDate, Types::Params::DateTime
|
|
20
|
-
# The human readable use and reproduction statement that applies
|
|
21
|
-
# example: Property rights reside with the repository. Literary rights reside with
|
|
22
|
-
# the creators of the documents or their heirs. To obtain permission to publish or
|
|
23
|
-
# reproduce, please contact the Public Services Librarian of the Dept. of Special Collections
|
|
24
|
-
# (http://library.stanford.edu/spc).
|
|
25
|
-
attribute? :useAndReproductionStatement, UseAndReproductionStatement.optional
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -394,13 +394,14 @@ module Cocina
|
|
|
394
394
|
end
|
|
395
395
|
|
|
396
396
|
def blank_ng_xml
|
|
397
|
-
Nokogiri::XML(
|
|
398
|
-
|
|
399
|
-
xmlns
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
397
|
+
Nokogiri::XML(
|
|
398
|
+
<<~XML
|
|
399
|
+
<mods xmlns="http://www.loc.gov/mods/v3"
|
|
400
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
401
|
+
version="3.6"
|
|
402
|
+
xsi:schemaLocation="http://www.loc.gov/mods/v3 http://www.loc.gov/standards/mods/v3/mods-3-6.xsd" />
|
|
403
|
+
XML
|
|
404
|
+
)
|
|
404
405
|
end
|
|
405
406
|
end
|
|
406
407
|
end
|
data/lib/cocina/models/title.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Cocina
|
|
4
4
|
module Models
|
|
5
5
|
class Title < Struct
|
|
6
|
+
attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
|
|
6
7
|
attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
7
8
|
attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
8
9
|
attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
@@ -39,7 +40,6 @@ module Cocina
|
|
|
39
40
|
attribute? :valueLanguage, DescriptiveValueLanguage.optional
|
|
40
41
|
# URL or other pointer to the location of the value of the descriptive element.
|
|
41
42
|
attribute? :valueAt, Types::Strict::String
|
|
42
|
-
attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -3,39 +3,172 @@
|
|
|
3
3
|
module Cocina
|
|
4
4
|
module Models
|
|
5
5
|
module Validators
|
|
6
|
-
#
|
|
6
|
+
# Validates Cocina model instances against the JSON schema.
|
|
7
|
+
#
|
|
8
|
+
# The schema uses OpenAPI 3.1.0 conventions with `unevaluatedProperties: false` to enforce
|
|
9
|
+
# strict validation. However, when schemas use `allOf` with `$ref` (which is common for
|
|
10
|
+
# composing mixins), json_schemer reports cascaded unevaluatedProperties errors that are
|
|
11
|
+
# not actionable.
|
|
12
|
+
#
|
|
13
|
+
# For example, if a schema has:
|
|
14
|
+
# AdminPolicy:
|
|
15
|
+
# allOf:
|
|
16
|
+
# - $ref: '#/$defs/AdminPolicyMixin' # defines properties
|
|
17
|
+
# unevaluatedProperties: false
|
|
18
|
+
#
|
|
19
|
+
# And an unexpected property appears at `/administrative/releaseTags`, json_schemer will
|
|
20
|
+
# report both:
|
|
21
|
+
# - The actual error at `/administrative/releaseTags` (actionable)
|
|
22
|
+
# - Cascaded errors for every known root property like `/label`, `/type` (noise)
|
|
23
|
+
#
|
|
24
|
+
# This happens because `unevaluatedProperties` semantics with `allOf`/$ref can be
|
|
25
|
+
# ambiguous. See https://github.com/davishmcclurg/json_schemer/issues/157
|
|
26
|
+
#
|
|
27
|
+
# To reduce noise while preserving actionable errors, this validator applies de-noising
|
|
28
|
+
# by keeping only:
|
|
29
|
+
#
|
|
30
|
+
# 1. All errors for nested properties (depth > 1)
|
|
31
|
+
# 2. Root-level errors for genuinely unknown properties (not in the model schema)
|
|
32
|
+
#
|
|
33
|
+
# This filters out cascaded root-level false positives without hiding real nested issues.
|
|
7
34
|
class JsonSchemaValidator
|
|
8
|
-
|
|
9
|
-
return unless clazz.name
|
|
35
|
+
SCHEMA_PATH = ::File.expand_path('../../../../schema.json', __dir__)
|
|
10
36
|
|
|
11
|
-
|
|
37
|
+
# @see #validate
|
|
38
|
+
def self.validate(...)
|
|
39
|
+
new(...).validate
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param clazz [Class] the Cocina model class being validated (e.g., Cocina::Models::DRO)
|
|
43
|
+
# @param attributes [Hash] the attributes of the model instance being validated
|
|
44
|
+
def initialize(clazz, attributes)
|
|
45
|
+
@clazz = clazz
|
|
46
|
+
@attributes = attributes
|
|
47
|
+
end
|
|
12
48
|
|
|
13
|
-
|
|
49
|
+
# Validates attributes against the Cocina model schema.
|
|
50
|
+
#
|
|
51
|
+
# Injects the cocinaVersion if the model includes it as an attribute, then validates
|
|
52
|
+
# the attributes against the schema definition for this model. De-noises
|
|
53
|
+
# unevaluatedProperties errors before raising a ValidationError.
|
|
54
|
+
#
|
|
55
|
+
# @return [NilClass] returns nil if validation passes
|
|
56
|
+
# @raise [ValidationError] if validation fails, with a de-noised error message
|
|
57
|
+
def validate
|
|
58
|
+
attributes['cocinaVersion'] = Cocina::Models::VERSION if clazz.attribute_names.include?(:cocinaVersion)
|
|
14
59
|
|
|
15
60
|
errors = schema.ref("#/$defs/#{method_name}").validate(attributes.as_json).to_a
|
|
16
|
-
return
|
|
61
|
+
return if errors.empty?
|
|
17
62
|
|
|
18
|
-
raise ValidationError, "When validating #{method_name}: " + errors.
|
|
63
|
+
raise ValidationError, "When validating #{method_name}: " + filtered_error_messages(errors).join(', ')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
attr_reader :clazz, :attributes
|
|
69
|
+
|
|
70
|
+
# @return [JSONSchemer::Schema]
|
|
71
|
+
def schema
|
|
72
|
+
@schema ||= JSONSchemer.schema(document)
|
|
19
73
|
end
|
|
20
74
|
|
|
21
75
|
# @return [Hash] a hash representation of the schema.json document
|
|
22
|
-
def
|
|
76
|
+
def document
|
|
23
77
|
@document ||= begin
|
|
24
|
-
file_content = ::File.read(
|
|
78
|
+
file_content = ::File.read(SCHEMA_PATH)
|
|
25
79
|
JSON.parse(file_content)
|
|
26
80
|
end
|
|
27
81
|
end
|
|
28
82
|
|
|
29
|
-
# @return [
|
|
30
|
-
def
|
|
31
|
-
@
|
|
83
|
+
# @return [String] the method name derived from the class name
|
|
84
|
+
def method_name
|
|
85
|
+
@method_name ||= clazz.name.split('::').last
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Array<String>] list of known root property names for the model class
|
|
89
|
+
def known_root_properties
|
|
90
|
+
@known_root_properties ||= clazz.attribute_names.map(&:to_s)
|
|
32
91
|
end
|
|
33
|
-
private_class_method :schema
|
|
34
92
|
|
|
35
|
-
|
|
36
|
-
|
|
93
|
+
# Filters and de-duplicates error messages.
|
|
94
|
+
#
|
|
95
|
+
# Applies unevaluatedProperties de-noising to reduce cascaded false-positive errors,
|
|
96
|
+
# then extracts the error message strings and removes duplicates.
|
|
97
|
+
#
|
|
98
|
+
# @param errors [Array<Hash>] errors returned by json_schemer validator
|
|
99
|
+
# @return [Array<String>] de-noised, unique error message strings
|
|
100
|
+
def filtered_error_messages(errors)
|
|
101
|
+
denoised_errors = filter_unevaluated_property_noise(errors)
|
|
102
|
+
denoised_errors.map { |error| error['error'] }.uniq
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Filters out cascaded unevaluatedProperties errors while preserving actionable ones.
|
|
106
|
+
#
|
|
107
|
+
# When unevaluatedProperties errors are present, this method keeps:
|
|
108
|
+
# - All non-unevaluatedProperties errors (unchanged)
|
|
109
|
+
# - All unevaluatedProperties errors for nested properties (depth > 1)
|
|
110
|
+
# - Root-level unevaluatedProperties errors for genuinely unknown properties
|
|
111
|
+
#
|
|
112
|
+
# It discards:
|
|
113
|
+
# - Root-level unevaluatedProperties errors for known model attributes
|
|
114
|
+
#
|
|
115
|
+
# This filtering leverages the class attribute schema to distinguish between
|
|
116
|
+
# cascaded noise (root-level disallowed errors for known properties) and genuine
|
|
117
|
+
# issues (unexpected properties at any depth).
|
|
118
|
+
#
|
|
119
|
+
# @param errors [Array<Hash>] errors from json_schemer validator
|
|
120
|
+
# @return [Array<Hash>] filtered error hashes
|
|
121
|
+
def filter_unevaluated_property_noise(errors)
|
|
122
|
+
unevaluated_errors = errors.select { |error| unevaluated_property_error?(error) }
|
|
123
|
+
return errors if unevaluated_errors.empty?
|
|
124
|
+
|
|
125
|
+
# Keep non-unevaluated errors and filtered unevaluated errors
|
|
126
|
+
non_unevaluated = errors.reject { |error| unevaluated_property_error?(error) }
|
|
127
|
+
filtered_unevaluated = unevaluated_errors.select do |error|
|
|
128
|
+
depth = pointer_depth(error['data_pointer'])
|
|
129
|
+
# Keep if: no depth data, nested path, or unknown root property
|
|
130
|
+
depth.nil? || depth > 1 || root_unknown_property_error?(error)
|
|
131
|
+
end
|
|
132
|
+
non_unevaluated + filtered_unevaluated
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Checks if an error represents an unknown property at the root level.
|
|
136
|
+
#
|
|
137
|
+
# A root-level error is actionable only if the property name is not in the model's
|
|
138
|
+
# defined attributes. This distinguishes between genuine unexpected properties and
|
|
139
|
+
# cascaded noise from allOf/$ref evaluation.
|
|
140
|
+
#
|
|
141
|
+
# @param error [Hash] json_schemer error hash
|
|
142
|
+
# @return [Boolean] true if error is for a root-level unknown property
|
|
143
|
+
def root_unknown_property_error?(error)
|
|
144
|
+
return false unless pointer_depth(error['data_pointer']) == 1
|
|
145
|
+
|
|
146
|
+
property_name = error['data_pointer'].split('/').last
|
|
147
|
+
!known_root_properties.include?(property_name)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Identifies errors related to unevaluatedProperties violations.
|
|
151
|
+
#
|
|
152
|
+
# @param error [Hash] json_schemer error hash
|
|
153
|
+
# @return [Boolean] true if error is an unevaluatedProperties violation
|
|
154
|
+
def unevaluated_property_error?(error)
|
|
155
|
+
error['schema_pointer']&.end_with?('/unevaluatedProperties') ||
|
|
156
|
+
error['error']&.include?('disallowed unevaluated property')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Calculates the depth of a JSON pointer path.
|
|
160
|
+
#
|
|
161
|
+
# A root-level property has depth 1 (e.g., `/label`), a nested property has depth > 1
|
|
162
|
+
# (e.g., `/administrative/releaseTags` has depth 2).
|
|
163
|
+
#
|
|
164
|
+
# @param pointer [String, nil] JSON pointer path (e.g., `/administrative/releaseTags`)
|
|
165
|
+
# @return [Integer, nil] path depth, or nil if pointer is nil
|
|
166
|
+
def pointer_depth(pointer)
|
|
167
|
+
return if pointer.nil?
|
|
168
|
+
return 0 if pointer.empty?
|
|
169
|
+
|
|
170
|
+
pointer.split('/').reject(&:empty?).length
|
|
37
171
|
end
|
|
38
|
-
private_class_method :schema_path
|
|
39
172
|
end
|
|
40
173
|
end
|
|
41
174
|
end
|