cocina-models 0.127.0 → 0.129.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/Gemfile.lock +16 -25
- data/form_source_codes.yml +600 -0
- data/lib/cocina/models/admin_policy_access_template.rb +11 -11
- data/lib/cocina/models/applies_to.rb +37 -3
- data/lib/cocina/models/descriptive_basic_value.rb +1 -3
- data/lib/cocina/models/descriptive_identifier.rb +2 -3
- data/lib/cocina/models/descriptive_value.rb +3 -3
- data/lib/cocina/models/dro_access.rb +11 -11
- data/lib/cocina/models/language.rb +1 -1
- data/lib/cocina/models/mapping/from_mods/description_builder.rb +5 -2
- data/lib/cocina/models/mapping/from_mods/form.rb +1 -1
- data/lib/cocina/models/mapping/from_mods/related_resource.rb +2 -1
- data/lib/cocina/models/related_resource.rb +0 -2
- data/lib/cocina/models/request_admin_policy.rb +1 -1
- data/lib/cocina/models/title.rb +3 -3
- data/lib/cocina/models/validatable.rb +20 -2
- data/lib/cocina/models/validators/composite_description_validator.rb +2 -1
- data/lib/cocina/models/validators/composite_structural_validator.rb +1 -1
- data/lib/cocina/models/validators/description_form_source_code_visitor_validator.rb +41 -0
- data/lib/cocina/models/validators/json_schema_validator.rb +50 -5
- data/lib/cocina/models/validators/validator.rb +13 -3
- data/lib/cocina/models/version.rb +1 -1
- data/lib/cocina/rspec/factories.rb +3 -9
- data/schema.json +166 -205
- metadata +3 -1
|
@@ -6,6 +6,17 @@ module Cocina
|
|
|
6
6
|
# an AdminPolicy. This is almost the same as DROAccess, but it provides no defaults
|
|
7
7
|
# and has no embargo.
|
|
8
8
|
class AdminPolicyAccessTemplate < Struct
|
|
9
|
+
# Access level.
|
|
10
|
+
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
11
|
+
attribute? :view, Types::Strict::String.optional.default('dark')
|
|
12
|
+
# Download access level.
|
|
13
|
+
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
14
|
+
attribute? :download, Types::Strict::String.optional.default('none')
|
|
15
|
+
# Not used for this access type, must be null.
|
|
16
|
+
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
17
|
+
attribute? :location, Types::Strict::String.optional
|
|
18
|
+
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
19
|
+
attribute? :controlledDigitalLending, Types::Strict::Bool.optional.default(false)
|
|
9
20
|
# The human readable copyright statement that applies
|
|
10
21
|
# example: Copyright World Trade Organization
|
|
11
22
|
attribute? :copyright, Copyright.optional
|
|
@@ -18,17 +29,6 @@ module Cocina
|
|
|
18
29
|
# The license governing reuse of the DRO. Should be an IRI for known licenses (i.e.
|
|
19
30
|
# CC, RightsStatement.org URI, etc.).
|
|
20
31
|
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')
|
|
21
|
-
# Access level.
|
|
22
|
-
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
23
|
-
attribute? :view, Types::Strict::String.optional.default('dark')
|
|
24
|
-
# Download access level.
|
|
25
|
-
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
26
|
-
attribute? :download, Types::Strict::String.optional.default('none')
|
|
27
|
-
# Not used for this access type, must be null.
|
|
28
|
-
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
29
|
-
attribute? :location, Types::Strict::String.optional
|
|
30
|
-
# Validation of this property is relaxed. See the schema.json for full validation.
|
|
31
|
-
attribute? :controlledDigitalLending, Types::Strict::Bool.optional.default(false)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -2,10 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
module Cocina
|
|
4
4
|
module Models
|
|
5
|
-
#
|
|
6
|
-
# which a descriptive element is applicable.
|
|
5
|
+
# Value model for appliesTo property.
|
|
7
6
|
class AppliesTo < Struct
|
|
8
|
-
attribute :
|
|
7
|
+
attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
8
|
+
attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
9
|
+
attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
10
|
+
# String or integer value of the descriptive element.
|
|
11
|
+
attribute? :value, Types::Nominal::Any
|
|
12
|
+
# Type of value provided by the descriptive element. See https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md
|
|
13
|
+
# for valid types.
|
|
14
|
+
attribute? :type, Types::Strict::String
|
|
15
|
+
# Status of the descriptive element value relative to other instances of the element.
|
|
16
|
+
attribute? :status, Types::Strict::String
|
|
17
|
+
# Code value of the descriptive element.
|
|
18
|
+
attribute? :code, Types::Strict::String
|
|
19
|
+
# URI value of the descriptive element.
|
|
20
|
+
attribute? :uri, Types::Strict::String
|
|
21
|
+
# Property model for indicating the encoding, standard, or syntax to which a value
|
|
22
|
+
# conforms (e.g. RDA).
|
|
23
|
+
attribute? :standard, Standard.optional
|
|
24
|
+
# Property model for indicating the encoding, standard, or syntax to which a value
|
|
25
|
+
# conforms (e.g. RDA).
|
|
26
|
+
attribute? :encoding, Standard.optional
|
|
27
|
+
# Property model for indicating the vocabulary, authority, or other origin for a term,
|
|
28
|
+
# code, or identifier.
|
|
29
|
+
attribute? :source, Source.optional
|
|
30
|
+
# The preferred display label to use for the descriptive element in access systems.
|
|
31
|
+
attribute? :displayLabel, Types::Strict::String.optional
|
|
32
|
+
# A term providing information about the circumstances of the statement (e.g., approximate
|
|
33
|
+
# dates).
|
|
34
|
+
attribute? :qualifier, Types::Strict::String
|
|
35
|
+
# Other information related to the descriptive element.
|
|
36
|
+
attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
37
|
+
# Language of the descriptive element value
|
|
38
|
+
attribute? :valueLanguage, DescriptiveValueLanguage.optional
|
|
39
|
+
# URL or other pointer to the location of the value of the descriptive element.
|
|
40
|
+
attribute? :valueAt, Types::Strict::String
|
|
41
|
+
# Identifiers and URIs associated with the descriptive element.
|
|
42
|
+
attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
|
|
9
43
|
end
|
|
10
44
|
end
|
|
11
45
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Cocina
|
|
4
4
|
module Models
|
|
5
5
|
# Basic value model for descriptive elements. Can only have one of value, parallelValue,
|
|
6
|
-
# groupedValue, or structuredValue.
|
|
6
|
+
# groupedValue, or structuredValue. Does not have identifier or appliesTo properties.
|
|
7
7
|
class DescriptiveBasicValue < Struct
|
|
8
8
|
attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
9
9
|
attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
@@ -25,8 +25,6 @@ module Cocina
|
|
|
25
25
|
# Property model for indicating the encoding, standard, or syntax to which a value
|
|
26
26
|
# conforms (e.g. RDA).
|
|
27
27
|
attribute? :encoding, Standard.optional
|
|
28
|
-
# Identifiers and URIs associated with the descriptive element.
|
|
29
|
-
attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
|
|
30
28
|
# Property model for indicating the vocabulary, authority, or other origin for a term,
|
|
31
29
|
# code, or identifier.
|
|
32
30
|
attribute? :source, Source.optional
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Cocina
|
|
4
4
|
module Models
|
|
5
|
-
#
|
|
6
|
-
# property to prevent infinite recursion.
|
|
5
|
+
# Default value model for identifiers.
|
|
7
6
|
class DescriptiveIdentifier < Struct
|
|
8
|
-
attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
|
|
9
7
|
attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
10
8
|
attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
11
9
|
attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
@@ -40,6 +38,7 @@ module Cocina
|
|
|
40
38
|
attribute? :valueLanguage, DescriptiveValueLanguage.optional
|
|
41
39
|
# URL or other pointer to the location of the value of the descriptive element.
|
|
42
40
|
attribute? :valueAt, Types::Strict::String
|
|
41
|
+
attribute :appliesTo, Types::Strict::Array.of(AppliesTo).default([].freeze)
|
|
43
42
|
end
|
|
44
43
|
end
|
|
45
44
|
end
|
|
@@ -4,7 +4,6 @@ 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)
|
|
8
7
|
attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
9
8
|
attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
10
9
|
attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
@@ -25,8 +24,6 @@ module Cocina
|
|
|
25
24
|
# Property model for indicating the encoding, standard, or syntax to which a value
|
|
26
25
|
# conforms (e.g. RDA).
|
|
27
26
|
attribute? :encoding, Standard.optional
|
|
28
|
-
# Identifiers and URIs associated with the descriptive element.
|
|
29
|
-
attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
|
|
30
27
|
# Property model for indicating the vocabulary, authority, or other origin for a term,
|
|
31
28
|
# code, or identifier.
|
|
32
29
|
attribute? :source, Source.optional
|
|
@@ -41,6 +38,9 @@ module Cocina
|
|
|
41
38
|
attribute? :valueLanguage, DescriptiveValueLanguage.optional
|
|
42
39
|
# URL or other pointer to the location of the value of the descriptive element.
|
|
43
40
|
attribute? :valueAt, Types::Strict::String
|
|
41
|
+
attribute :appliesTo, Types::Strict::Array.of(AppliesTo).default([].freeze)
|
|
42
|
+
# Identifiers and URIs associated with the descriptive element.
|
|
43
|
+
attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -3,6 +3,17 @@
|
|
|
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)
|
|
6
17
|
# The human readable copyright statement that applies
|
|
7
18
|
# example: Copyright World Trade Organization
|
|
8
19
|
attribute? :copyright, Copyright.optional
|
|
@@ -16,17 +27,6 @@ module Cocina
|
|
|
16
27
|
# The license governing reuse of the DRO. Should be an IRI for known licenses (i.e.
|
|
17
28
|
# CC, RightsStatement.org URI, etc.).
|
|
18
29
|
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
|
|
@@ -5,7 +5,7 @@ module Cocina
|
|
|
5
5
|
# Languages, scripts, symbolic systems, and notations used in all or part of a resource
|
|
6
6
|
# or its descriptive metadata.
|
|
7
7
|
class Language < Struct
|
|
8
|
-
attribute :appliesTo, Types::Strict::Array.of(
|
|
8
|
+
attribute :appliesTo, Types::Strict::Array.of(AppliesTo).default([].freeze)
|
|
9
9
|
# Code value of the descriptive element.
|
|
10
10
|
attribute? :code, Types::Strict::String
|
|
11
11
|
# The preferred display label to use for the descriptive element in access systems.
|
|
@@ -37,8 +37,10 @@ module Cocina
|
|
|
37
37
|
@notifier = notifier
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# @param [Boolean] include_related_resource whether to map nested relatedItem elements;
|
|
41
|
+
# false when building a relatedItem, since RelatedResource cannot itself nest a relatedResource
|
|
40
42
|
# @return [Hash] a hash that can be mapped to a cocina description model
|
|
41
|
-
def build(resource_element:, purl: nil, require_title: true)
|
|
43
|
+
def build(resource_element:, purl: nil, require_title: true, include_related_resource: true)
|
|
42
44
|
cocina_description = {}
|
|
43
45
|
title_result = @title_builder.build(resource_element: resource_element, require_title: require_title,
|
|
44
46
|
notifier: notifier)
|
|
@@ -47,7 +49,8 @@ module Cocina
|
|
|
47
49
|
purl_value = purl || Purl.primary_purl_value(resource_element, purl)
|
|
48
50
|
cocina_description[:purl] = purl_value if purl_value
|
|
49
51
|
|
|
50
|
-
BUILDERS
|
|
52
|
+
builders = include_related_resource ? BUILDERS : BUILDERS.except(:relatedResource)
|
|
53
|
+
builders.each do |description_property, builder|
|
|
51
54
|
result = builder.build(resource_element: resource_element, description_builder: self,
|
|
52
55
|
purl: purl_value)
|
|
53
56
|
cocina_description.merge!(description_property => result) if result.present?
|
|
@@ -88,7 +88,7 @@ module Cocina
|
|
|
88
88
|
uri: ValueURI.sniff(subject_node['valueURI'], notifier)
|
|
89
89
|
}.tap do |attrs|
|
|
90
90
|
source = {
|
|
91
|
-
|
|
91
|
+
value: subject_node['authority'],
|
|
92
92
|
uri: subject_node['authorityURI']
|
|
93
93
|
}.compact
|
|
94
94
|
attrs[:source] = source if source.present?
|
|
@@ -80,7 +80,8 @@ module Cocina
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def build_related_item(related_item)
|
|
83
|
-
description_builder.build(resource_element: related_item, require_title: false
|
|
83
|
+
description_builder.build(resource_element: related_item, require_title: false,
|
|
84
|
+
include_related_resource: false).tap do |item|
|
|
84
85
|
item[:displayLabel] = related_item['displayLabel']
|
|
85
86
|
if related_item['type']
|
|
86
87
|
item[:type] = normalized_type_for(related_item['type'])
|
|
@@ -38,8 +38,6 @@ module Cocina
|
|
|
38
38
|
attribute? :purl, Purl.optional
|
|
39
39
|
# Information about how to access digital and physical versions of the object.
|
|
40
40
|
attribute? :access, DescriptiveAccessMetadata.optional
|
|
41
|
-
# Other resources associated with the related resource.
|
|
42
|
-
attribute :relatedResource, Types::Strict::Array.of(RelatedResource).default([].freeze)
|
|
43
41
|
# Information about this resource description.
|
|
44
42
|
attribute? :adminMetadata, DescriptiveAdminMetadata.optional
|
|
45
43
|
# The version of the related resource.
|
|
@@ -20,7 +20,7 @@ module Cocina
|
|
|
20
20
|
attribute(:administrative, AdminPolicyAdministrative.default { AdminPolicyAdministrative.new })
|
|
21
21
|
# Description that is included in a request to create a DRO. This is the same as a
|
|
22
22
|
# Description, except excludes PURL.
|
|
23
|
-
attribute
|
|
23
|
+
attribute(:description, RequestDescription.default { RequestDescription.new })
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
data/lib/cocina/models/title.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
module Cocina
|
|
4
4
|
module Models
|
|
5
5
|
class Title < Struct
|
|
6
|
-
attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
|
|
7
6
|
attribute :structuredValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
8
7
|
attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
9
8
|
attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
|
@@ -24,8 +23,6 @@ module Cocina
|
|
|
24
23
|
# Property model for indicating the encoding, standard, or syntax to which a value
|
|
25
24
|
# conforms (e.g. RDA).
|
|
26
25
|
attribute? :encoding, Standard.optional
|
|
27
|
-
# Identifiers and URIs associated with the descriptive element.
|
|
28
|
-
attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
|
|
29
26
|
# Property model for indicating the vocabulary, authority, or other origin for a term,
|
|
30
27
|
# code, or identifier.
|
|
31
28
|
attribute? :source, Source.optional
|
|
@@ -40,6 +37,9 @@ module Cocina
|
|
|
40
37
|
attribute? :valueLanguage, DescriptiveValueLanguage.optional
|
|
41
38
|
# URL or other pointer to the location of the value of the descriptive element.
|
|
42
39
|
attribute? :valueAt, Types::Strict::String
|
|
40
|
+
attribute :appliesTo, Types::Strict::Array.of(AppliesTo).default([].freeze)
|
|
41
|
+
# Identifiers and URIs associated with the descriptive element.
|
|
42
|
+
attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -12,7 +12,18 @@ module Cocina
|
|
|
12
12
|
Thread.current[:top_level_validate] = validate unless Thread.current.key?(:top_level_validate)
|
|
13
13
|
|
|
14
14
|
Validators::Validator.validate(self, attributes) if Thread.current[:top_level_validate]
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
# Tracks that any Validatable construction triggered by `super` below is nested inside this one, so
|
|
17
|
+
# JsonSchemaValidator can skip it: this object's own schema check above already covers.
|
|
18
|
+
# See JsonSchemaValidator#validate.
|
|
19
|
+
# In practice, this avoids doing schema validation on a DRO / AdminPolicy / Collection and
|
|
20
|
+
# its nested Description.
|
|
21
|
+
Thread.current[:cocina_construction_depth] = Thread.current[:cocina_construction_depth].to_i + 1
|
|
22
|
+
begin
|
|
23
|
+
super(attributes, safe, &)
|
|
24
|
+
ensure
|
|
25
|
+
Thread.current[:cocina_construction_depth] -= 1
|
|
26
|
+
end
|
|
16
27
|
ensure
|
|
17
28
|
Thread.current[:top_level_validate] = nil
|
|
18
29
|
end
|
|
@@ -20,7 +31,14 @@ module Cocina
|
|
|
20
31
|
|
|
21
32
|
def new(*args)
|
|
22
33
|
validate = args.first.delete(:validate) if args.present?
|
|
23
|
-
|
|
34
|
+
|
|
35
|
+
Thread.current[:cocina_construction_depth] = Thread.current[:cocina_construction_depth].to_i + 1
|
|
36
|
+
begin
|
|
37
|
+
new_model = super
|
|
38
|
+
ensure
|
|
39
|
+
Thread.current[:cocina_construction_depth] -= 1
|
|
40
|
+
end
|
|
41
|
+
|
|
24
42
|
Validators::Validator.validate(new_model.class, new_model) if validate || validate.nil?
|
|
25
43
|
new_model
|
|
26
44
|
end
|
|
@@ -11,6 +11,7 @@ module Cocina
|
|
|
11
11
|
DescriptionRoleSourceCodeVisitorValidator,
|
|
12
12
|
DescriptionNameSourceCodeVisitorValidator,
|
|
13
13
|
DescriptionFormResourceTypeVisitorValidator,
|
|
14
|
+
DescriptionFormSourceCodeVisitorValidator,
|
|
14
15
|
DescriptionLanguageCodeVisitorValidator,
|
|
15
16
|
DescriptionSubjectSourceCodeVisitorValidator,
|
|
16
17
|
DescriptionValuesVisitorValidator,
|
|
@@ -30,7 +31,7 @@ module Cocina
|
|
|
30
31
|
def initialize(clazz, attributes, validators: VALIDATORS)
|
|
31
32
|
@clazz = clazz
|
|
32
33
|
@attributes = attributes
|
|
33
|
-
@validators = validators.map(&:new)
|
|
34
|
+
@validators = meets_preconditions? ? validators.map(&:new) : []
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def validate
|
|
@@ -18,7 +18,7 @@ module Cocina
|
|
|
18
18
|
def initialize(clazz, attributes, validators: VALIDATORS)
|
|
19
19
|
@clazz = clazz
|
|
20
20
|
@attributes = attributes
|
|
21
|
-
@validators = validators.map { |v| v.new(attributes) }
|
|
21
|
+
@validators = meets_preconditions? ? validators.map { |v| v.new(attributes) } : []
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def validate
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cocina
|
|
4
|
+
module Models
|
|
5
|
+
module Validators
|
|
6
|
+
# Validates form.source.code values against form_source_codes.yml.
|
|
7
|
+
class DescriptionFormSourceCodeVisitorValidator < BaseDescriptionVisitorValidator
|
|
8
|
+
def validate!
|
|
9
|
+
return if error_paths.empty?
|
|
10
|
+
|
|
11
|
+
raise ValidationError, "Unrecognized form source codes in description: #{error_paths.join(', ')}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def visit_hash(hash:, path:)
|
|
15
|
+
return unless form_entry_path?(path)
|
|
16
|
+
|
|
17
|
+
source_code = hash.dig(:source, :code)
|
|
18
|
+
return unless source_code
|
|
19
|
+
|
|
20
|
+
error_paths << "#{path_to_s(path)}.source.code (#{source_code})" unless valid_codes.include?(source_code.downcase)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def error_paths
|
|
26
|
+
@error_paths ||= []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def form_entry_path?(path)
|
|
30
|
+
path.length >= 2 && path[-1].is_a?(Integer) && path[-2].to_s == 'form'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# rubocop:disable Style/ClassVars
|
|
34
|
+
def valid_codes
|
|
35
|
+
@@valid_codes ||= YAML.load_file(::File.expand_path('../../../../form_source_codes.yml', __dir__)).to_set(&:downcase)
|
|
36
|
+
end
|
|
37
|
+
# rubocop:enable Style/ClassVars
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -25,6 +25,11 @@ module Cocina
|
|
|
25
25
|
|
|
26
26
|
# @see #validate
|
|
27
27
|
def self.validate(...)
|
|
28
|
+
# Skip when nested inside another Validatable's construction: the enclosing object's own
|
|
29
|
+
# schema check already covers this, so re-evaluating it here would be
|
|
30
|
+
# redundant. See Validatable#new.
|
|
31
|
+
return if Thread.current[:cocina_construction_depth].to_i.positive?
|
|
32
|
+
|
|
28
33
|
new(...).validate
|
|
29
34
|
end
|
|
30
35
|
|
|
@@ -67,7 +72,7 @@ module Cocina
|
|
|
67
72
|
evaluation = self.class.validator_for(method_name).evaluate(attributes.as_json)
|
|
68
73
|
return if evaluation.valid?
|
|
69
74
|
|
|
70
|
-
raise ValidationError, "When validating #{method_name}: " + filtered_error_messages(evaluation).join('
|
|
75
|
+
raise ValidationError, "When validating #{method_name}: " + filtered_error_messages(evaluation).join('; ')
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
private
|
|
@@ -90,7 +95,7 @@ module Cocina
|
|
|
90
95
|
denoised.flat_map { |d| format_detail(d) }.uniq
|
|
91
96
|
end
|
|
92
97
|
|
|
93
|
-
#
|
|
98
|
+
# Removes three categories of cascade noise from errors:
|
|
94
99
|
#
|
|
95
100
|
# 1. falseSchema errors — every `unevaluatedProperties: false` on a sub-path emits
|
|
96
101
|
# a "False schema does not allow …" entry for each matched value. These are
|
|
@@ -100,8 +105,28 @@ module Cocina
|
|
|
100
105
|
# is a known model attribute — these arise because allOf/$ref composition means
|
|
101
106
|
# the validator can't prove top-level properties were "evaluated", even though
|
|
102
107
|
# they are valid model fields.
|
|
108
|
+
#
|
|
109
|
+
# 3. anyOf branch noise — when each branch requires a different property, all failing
|
|
110
|
+
# branches emit a separate "required" error at the same path. Collapse into one
|
|
111
|
+
# "needs to include at least one of: …" message.
|
|
103
112
|
def denoise(details)
|
|
104
|
-
details.reject { |
|
|
113
|
+
filtered = details.reject { |detail| false_schema_noise?(detail) || root_unevaluated_noise?(detail) }
|
|
114
|
+
collapse_anyof_required(filtered)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def collapse_anyof_required(details)
|
|
118
|
+
# instanceLocation is the input data that failed. Group errors for each failing data location.
|
|
119
|
+
details.group_by { |detail| detail[:instanceLocation] }.flat_map do |loc, group|
|
|
120
|
+
# splits errors into "required" anyOf errors and others (minItems, unevaluatedProperties)
|
|
121
|
+
required, others = group.partition { |detail| detail[:errors].keys == ['required'] }
|
|
122
|
+
next group if required.size <= 1
|
|
123
|
+
|
|
124
|
+
# Extract property name from "required" messages such as, '"url" is a required property'
|
|
125
|
+
props = required.filter_map { |detail| detail[:errors]['required'][/"([^"]+)"/, 1] }.uniq
|
|
126
|
+
# Put the data path at the start of the message; blank instanceLocation so format_detail won't also append it
|
|
127
|
+
msg = "#{loc} needs to include at least one of the following: #{props.join(', ')}"
|
|
128
|
+
others + [required.first.merge(instanceLocation: '', errors: { 'required' => msg })]
|
|
129
|
+
end
|
|
105
130
|
end
|
|
106
131
|
|
|
107
132
|
def false_schema_noise?(detail)
|
|
@@ -120,12 +145,32 @@ module Cocina
|
|
|
120
145
|
unexpected.all? { |prop| known_root_properties.include?(prop) }
|
|
121
146
|
end
|
|
122
147
|
|
|
148
|
+
# Formats error details hash into more user-friendly messages
|
|
149
|
+
# A detail can carry multiple errors (one per JSON Schema keyword), so this returns an array.
|
|
150
|
+
# minItems errors get a dedicated formatter; everything else uses the generic one.
|
|
123
151
|
def format_detail(detail)
|
|
124
152
|
loc = detail[:instanceLocation]
|
|
125
|
-
detail[:errors].map do |
|
|
126
|
-
|
|
153
|
+
detail[:errors].map do |keyword, message|
|
|
154
|
+
keyword == 'minItems' ? format_min_items(message, loc) : format_generic(message, loc)
|
|
127
155
|
end
|
|
128
156
|
end
|
|
157
|
+
|
|
158
|
+
# Rewrites messages regarding minItems validation
|
|
159
|
+
# "[] has less than 1 item at /event/0/date " is rewritten to "/event/0/date is empty but should have at least 1 item"
|
|
160
|
+
# Arrays with items but fewer than the minimum will be rewritten to "/event/0/date should have at least 2 items"
|
|
161
|
+
def format_min_items(message, loc)
|
|
162
|
+
# extract minimum number of items from message
|
|
163
|
+
min = message[/less than (\d+)/, 1] || '1'
|
|
164
|
+
# pluralize "item" when appropriate
|
|
165
|
+
items = min == '1' ? 'item' : 'items'
|
|
166
|
+
# determine if the message is about an empty array or an non-empty array without enough items
|
|
167
|
+
prefix = message.start_with?('[]') ? "#{loc} is empty but" : loc.to_s
|
|
168
|
+
"#{prefix} should have at least #{min} #{items}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def format_generic(message, loc)
|
|
172
|
+
loc.empty? ? message : "#{message} at #{loc}"
|
|
173
|
+
end
|
|
129
174
|
end
|
|
130
175
|
end
|
|
131
176
|
end
|
|
@@ -16,12 +16,22 @@ module Cocina
|
|
|
16
16
|
].freeze
|
|
17
17
|
|
|
18
18
|
def self.validate(clazz, attributes, validators: VALIDATORS)
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
hash = attributes_hash(attributes)
|
|
20
|
+
validators.each { |validator| validator.validate(clazz, hash) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Hash] attributes with any embedded Cocina model instances flattened to hashes
|
|
24
|
+
def self.attributes_hash(attributes)
|
|
25
|
+
# Dry::Struct#to_h is already fully recursive (Dry::Struct::Hashify flattens nested
|
|
26
|
+
# structs all the way down), so a Cocina model instance's #to_h has no embedded
|
|
27
|
+
# Cocina model instances left to flatten and the deep walk below would be a no-op.
|
|
28
|
+
return attributes.to_h.with_indifferent_access if attributes.class.name.starts_with?('Cocina::Models')
|
|
29
|
+
|
|
30
|
+
attributes.to_h.deep_transform_values do |value|
|
|
21
31
|
value.class.name.starts_with?('Cocina::Models') ? value.to_h : value
|
|
22
32
|
end.with_indifferent_access
|
|
23
|
-
validators.each { |validator| validator.validate(clazz, attributes_hash) }
|
|
24
33
|
end
|
|
34
|
+
private_class_method :attributes_hash
|
|
25
35
|
|
|
26
36
|
def self.deep_transform_values(object, ...)
|
|
27
37
|
case object
|
|
@@ -48,7 +48,6 @@ module Cocina
|
|
|
48
48
|
type: Cocina::Models::ObjectType.object,
|
|
49
49
|
id: 'druid:bc234fg5678',
|
|
50
50
|
version: 1,
|
|
51
|
-
label: 'factory DRO label',
|
|
52
51
|
title: 'factory DRO title',
|
|
53
52
|
source_id: 'sul:1234',
|
|
54
53
|
admin_policy_id: 'druid:hv992ry2431'
|
|
@@ -60,7 +59,6 @@ module Cocina
|
|
|
60
59
|
type: Cocina::Models::ObjectType.collection,
|
|
61
60
|
id: 'druid:bb222ff5555',
|
|
62
61
|
version: 1,
|
|
63
|
-
label: 'factory collection label',
|
|
64
62
|
title: 'factory collection title',
|
|
65
63
|
admin_policy_id: 'druid:hv992ry2431',
|
|
66
64
|
source_id: 'sulcollection:1234'
|
|
@@ -72,7 +70,6 @@ module Cocina
|
|
|
72
70
|
type: Cocina::Models::ObjectType.admin_policy,
|
|
73
71
|
id: 'druid:cb432gf8765',
|
|
74
72
|
version: 1,
|
|
75
|
-
label: 'factory APO label',
|
|
76
73
|
title: 'factory APO title',
|
|
77
74
|
admin_policy_id: 'druid:hv992ry2431',
|
|
78
75
|
agreement_id: 'druid:hp308wm0436'
|
|
@@ -97,12 +94,11 @@ module Cocina
|
|
|
97
94
|
end
|
|
98
95
|
|
|
99
96
|
# rubocop:disable Metrics/ParameterLists
|
|
100
|
-
def self.build_request_dro_properties(type:, version:,
|
|
97
|
+
def self.build_request_dro_properties(type:, version:, title:, source_id:, admin_policy_id:,
|
|
101
98
|
barcode: nil, catkeys: [], folio_instance_hrids: [], collection_ids: [])
|
|
102
99
|
{
|
|
103
100
|
type: type,
|
|
104
101
|
version: version,
|
|
105
|
-
label: label,
|
|
106
102
|
access: {},
|
|
107
103
|
administrative: { hasAdminPolicy: admin_policy_id },
|
|
108
104
|
description: {
|
|
@@ -143,12 +139,11 @@ module Cocina
|
|
|
143
139
|
end
|
|
144
140
|
|
|
145
141
|
# rubocop:disable Metrics/ParameterLists
|
|
146
|
-
def self.build_request_collection_properties(type:, version:,
|
|
142
|
+
def self.build_request_collection_properties(type:, version:, title:, admin_policy_id:, source_id: nil,
|
|
147
143
|
catkeys: [], folio_instance_hrids: [])
|
|
148
144
|
{
|
|
149
145
|
type: type,
|
|
150
146
|
version: version,
|
|
151
|
-
label: label,
|
|
152
147
|
access: {},
|
|
153
148
|
administrative: { hasAdminPolicy: admin_policy_id },
|
|
154
149
|
description: {
|
|
@@ -204,7 +199,7 @@ module Cocina
|
|
|
204
199
|
end
|
|
205
200
|
|
|
206
201
|
# rubocop:disable Metrics/ParameterLists
|
|
207
|
-
def self.build_request_admin_policy_properties(type:, version:,
|
|
202
|
+
def self.build_request_admin_policy_properties(type:, version:, title:,
|
|
208
203
|
admin_policy_id:, agreement_id:,
|
|
209
204
|
use_statement: nil, copyright: nil, license: nil,
|
|
210
205
|
registration_workflow: nil, collections_for_registration: nil,
|
|
@@ -212,7 +207,6 @@ module Cocina
|
|
|
212
207
|
{
|
|
213
208
|
type: type,
|
|
214
209
|
version: version,
|
|
215
|
-
label: label,
|
|
216
210
|
administrative: {
|
|
217
211
|
hasAdminPolicy: admin_policy_id,
|
|
218
212
|
hasAgreement: agreement_id,
|