cocina-models 0.71.0 → 0.72.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cocina/generator/generator.rb +0 -1
- data/lib/cocina/models/catalog_link.rb +3 -1
- data/lib/cocina/models/title_builder.rb +16 -16
- data/lib/cocina/models/validatable.rb +2 -2
- data/lib/cocina/models/validators/catalog_links_validator.rb +54 -0
- data/lib/cocina/models/validators/dark_validator.rb +79 -0
- data/lib/cocina/models/validators/open_api_validator.rb +53 -0
- data/lib/cocina/models/validators/validator.rb +16 -0
- data/lib/cocina/models/version.rb +1 -1
- data/lib/cocina/models/vocabulary.rb +9 -0
- data/openapi.yml +13 -3
- metadata +9 -6
- data/lib/cocina/models/validator.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5516cbeff2267e688f6a09b34e4a0c715a1aea91ddae03d10060bed3d364c5a
|
4
|
+
data.tar.gz: f47bd2a4ca35c95d3566883707d39a2480c0b7e18ee28bc49042aa90b24e104c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 262cdfa4049c7325751645a578b2129f0f5cc4737198b82bea5728a6d693398bf2ab589dea2ae912de2de09e6abd1e3a581b84828f3ebf85ed64ba4c3a71dc25
|
7
|
+
data.tar.gz: 977677a8f5de44340117f3d2fb6ba8553e21c3c61c1770b18d44652008de6e26a6ada747de9546e993a0e4231e2263ad531d97491c5467d8aa8e18f2a303f340
|
@@ -5,7 +5,9 @@ module Cocina
|
|
5
5
|
class CatalogLink < Struct
|
6
6
|
# Catalog that is the source of the linked record.
|
7
7
|
# example: symphony
|
8
|
-
attribute :catalog, Types::Strict::String
|
8
|
+
attribute :catalog, Types::Strict::String.enum('symphony', 'previous symphony')
|
9
|
+
# Only one of the catkeys should be designated for refreshing. This means that this key is the one used to pull metadata from the catalog if there is more than one key present.
|
10
|
+
attribute :refresh, Types::Strict::Bool.default(false)
|
9
11
|
# Record identifier that is unique within the context of the linked record's catalog.
|
10
12
|
# example: 11403803
|
11
13
|
attribute :catalogRecordId, Types::Strict::String
|
@@ -46,8 +46,7 @@ module Cocina
|
|
46
46
|
result = if cocina_title.value
|
47
47
|
cocina_title.value
|
48
48
|
elsif cocina_title.structuredValue.present?
|
49
|
-
title_from_structured_values(cocina_title
|
50
|
-
non_sorting_char_count(cocina_title))
|
49
|
+
title_from_structured_values(cocina_title)
|
51
50
|
elsif cocina_title.parallelValue.present?
|
52
51
|
return build(cocina_title.parallelValue)
|
53
52
|
end
|
@@ -104,20 +103,17 @@ module Cocina
|
|
104
103
|
# rubocop:disable Metrics/PerceivedComplexity
|
105
104
|
# rubocop:disable Metrics/MethodLength
|
106
105
|
# rubocop:disable Metrics/AbcSize
|
107
|
-
# @param [
|
108
|
-
# @param [Integer] the length of the non_sorting_characters
|
106
|
+
# @param [Cocina::Models::Title] title with structured values
|
109
107
|
# @return [String] the title value from combining the pieces of the structured_values by type and order
|
110
108
|
# with desired punctuation per specs
|
111
|
-
def title_from_structured_values(
|
109
|
+
def title_from_structured_values(title)
|
112
110
|
structured_title = ''
|
113
111
|
part_name_number = ''
|
114
112
|
# combine pieces of the cocina structuredValue into a single title
|
115
|
-
|
113
|
+
title.structuredValue.each do |structured_value|
|
116
114
|
# There can be a structuredValue inside a structuredValue. For example,
|
117
115
|
# a uniform title where both the name and the title have internal StructuredValue
|
118
|
-
if structured_value.structuredValue.present?
|
119
|
-
return title_from_structured_values(structured_value.structuredValue, non_sorting_char_count)
|
120
|
-
end
|
116
|
+
return title_from_structured_values(structured_value) if structured_value.structuredValue.present?
|
121
117
|
|
122
118
|
value = structured_value.value&.strip
|
123
119
|
next unless value
|
@@ -125,8 +121,7 @@ module Cocina
|
|
125
121
|
# additional types: name, uniform ...
|
126
122
|
case structured_value.type&.downcase
|
127
123
|
when 'nonsorting characters'
|
128
|
-
|
129
|
-
non_sort_value = "#{value}#{' ' * non_sorting_size}"
|
124
|
+
non_sort_value = "#{value}#{non_sorting_padding(title, value)}"
|
130
125
|
structured_title = if structured_title.present?
|
131
126
|
"#{structured_title}#{non_sort_value}"
|
132
127
|
else
|
@@ -134,7 +129,7 @@ module Cocina
|
|
134
129
|
end
|
135
130
|
when 'part name', 'part number'
|
136
131
|
if part_name_number.blank?
|
137
|
-
part_name_number = part_name_number(
|
132
|
+
part_name_number = part_name_number(title.structuredValue)
|
138
133
|
structured_title = if !add_punctuation?
|
139
134
|
[structured_title, part_name_number].join(' ')
|
140
135
|
elsif structured_title.present?
|
@@ -168,11 +163,16 @@ module Cocina
|
|
168
163
|
title.sub(%r{[ .,;:/\\]+$}, '')
|
169
164
|
end
|
170
165
|
|
171
|
-
def
|
166
|
+
def non_sorting_padding(title, non_sorting_value)
|
172
167
|
non_sort_note = title.note&.find { |note| note.type&.downcase == 'nonsorting character count' }
|
173
|
-
|
174
|
-
|
175
|
-
|
168
|
+
if non_sort_note
|
169
|
+
padding_count = [non_sort_note.value.to_i - non_sorting_value.length, 0].max
|
170
|
+
' ' * padding_count
|
171
|
+
elsif ['\'', '-'].include?(non_sorting_value.last)
|
172
|
+
''
|
173
|
+
else
|
174
|
+
' '
|
175
|
+
end
|
176
176
|
end
|
177
177
|
|
178
178
|
# combine part name and part number:
|
@@ -8,7 +8,7 @@ module Cocina
|
|
8
8
|
|
9
9
|
class_methods do
|
10
10
|
def new(attributes = default_attributes, safe = false, validate = true, &block)
|
11
|
-
Validator.validate(self, attributes.with_indifferent_access) if validate
|
11
|
+
Validators::Validator.validate(self, attributes.with_indifferent_access) if validate
|
12
12
|
super(attributes, safe, &block)
|
13
13
|
end
|
14
14
|
end
|
@@ -16,7 +16,7 @@ module Cocina
|
|
16
16
|
def new(*args)
|
17
17
|
validate = args.first.delete(:validate) if args.present?
|
18
18
|
new_model = super(*args)
|
19
|
-
Validator.validate(new_model.class, new_model.to_h) if
|
19
|
+
Validators::Validator.validate(new_model.class, new_model.to_h) if validate || validate.nil?
|
20
20
|
new_model
|
21
21
|
end
|
22
22
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Validators
|
6
|
+
# Validates that only a single CatalogLink has refresh set to true
|
7
|
+
class CatalogLinksValidator
|
8
|
+
MAX_REFRESH_CATALOG_LINKS = 1
|
9
|
+
|
10
|
+
def self.validate(clazz, attributes)
|
11
|
+
new(clazz, attributes).validate
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(clazz, attributes)
|
15
|
+
@clazz = clazz
|
16
|
+
@attributes = attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate
|
20
|
+
return unless meets_preconditions?
|
21
|
+
|
22
|
+
return if refresh_catalog_links.length <= MAX_REFRESH_CATALOG_LINKS
|
23
|
+
|
24
|
+
raise ValidationError, "Multiple catalog links have 'refresh' property set to true " \
|
25
|
+
"(only one allowed) #{refresh_catalog_links}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :clazz, :attributes
|
31
|
+
|
32
|
+
def meets_preconditions?
|
33
|
+
(dro? || collection?) && Array(attributes.dig(:identification, :catalogLinks)).any?
|
34
|
+
end
|
35
|
+
|
36
|
+
def refresh_catalog_links
|
37
|
+
attributes.dig(:identification, :catalogLinks).select { |catalog_link| catalog_link[:refresh] }
|
38
|
+
end
|
39
|
+
|
40
|
+
def dro?
|
41
|
+
(clazz::TYPES & DRO::TYPES).any?
|
42
|
+
rescue NameError
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
def collection?
|
47
|
+
(clazz::TYPES & Collection::TYPES).any?
|
48
|
+
rescue NameError
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,79 @@
|
|
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 DarkValidator
|
8
|
+
def self.validate(clazz, attributes)
|
9
|
+
new(clazz, attributes).validate
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(clazz, attributes)
|
13
|
+
@clazz = clazz
|
14
|
+
@attributes = attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate
|
18
|
+
return unless meets_preconditions?
|
19
|
+
|
20
|
+
return if invalid_files.empty?
|
21
|
+
|
22
|
+
raise ValidationError, 'Not all files have dark access and/or are unshelved ' \
|
23
|
+
"when object access is dark: #{invalid_filenames}"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :clazz, :attributes
|
29
|
+
|
30
|
+
def meets_preconditions?
|
31
|
+
dro? && attributes.dig(:access, :view) == 'dark'
|
32
|
+
end
|
33
|
+
|
34
|
+
def dro?
|
35
|
+
(clazz::TYPES & DRO::TYPES).any?
|
36
|
+
rescue NameError
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def invalid_files
|
41
|
+
@invalid_files ||=
|
42
|
+
[].tap do |invalid_files|
|
43
|
+
files.each do |file|
|
44
|
+
invalid_files << file if invalid?(file)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def invalid_filenames
|
50
|
+
invalid_files.map { |invalid_file| invalid_file[:filename] || invalid_file[:label] }
|
51
|
+
end
|
52
|
+
|
53
|
+
def invalid?(file)
|
54
|
+
# Ignore if a WARC
|
55
|
+
return false if file[:hasMimeType] == 'application/warc'
|
56
|
+
|
57
|
+
return true if file.dig(:administrative, :shelve)
|
58
|
+
return true if file.dig(:access, :view) != 'dark'
|
59
|
+
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
def files
|
64
|
+
[].tap do |files|
|
65
|
+
next if attributes.dig(:structural, :contains).nil?
|
66
|
+
|
67
|
+
attributes[:structural][:contains].each do |fileset|
|
68
|
+
next if fileset.dig(:structural, :contains).nil?
|
69
|
+
|
70
|
+
fileset[:structural][:contains].each do |file|
|
71
|
+
files << file
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Validators
|
6
|
+
# Perform validation against openapi
|
7
|
+
class OpenApiValidator
|
8
|
+
def self.validate(clazz, attributes)
|
9
|
+
return unless clazz.name
|
10
|
+
|
11
|
+
method_name = clazz.name.split('::').last
|
12
|
+
request_operation = root.request_operation(:post, "/validate/#{method_name}")
|
13
|
+
|
14
|
+
# JSON.parse forces serialization of objects like DateTime.
|
15
|
+
json_attributes = JSON.parse(attributes.to_json)
|
16
|
+
# Inject cocinaVersion if needed and not present.
|
17
|
+
if operation_has_cocina_version?(request_operation) && !json_attributes.include?('cocinaVersion')
|
18
|
+
json_attributes['cocinaVersion'] = Cocina::Models::VERSION
|
19
|
+
end
|
20
|
+
|
21
|
+
request_operation.validate_request_body('application/json', json_attributes)
|
22
|
+
rescue OpenAPIParser::OpenAPIError => e
|
23
|
+
raise ValidationError, e.message
|
24
|
+
end
|
25
|
+
|
26
|
+
# rubocop:disable Metrics/AbcSize
|
27
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
28
|
+
def self.operation_has_cocina_version?(request_operation)
|
29
|
+
schema = request_operation.operation_object.request_body.content['application/json'].schema
|
30
|
+
all_of_properties = Array(schema.all_of&.flat_map { |all_of| all_of.properties&.keys }).compact
|
31
|
+
one_of_properties = Array(schema.one_of&.flat_map { |one_of| one_of.properties&.keys }).compact
|
32
|
+
properties = Array(schema.properties&.keys)
|
33
|
+
(properties + all_of_properties + one_of_properties).include?('cocinaVersion')
|
34
|
+
end
|
35
|
+
# rubocop:enable Metrics/AbcSize
|
36
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
37
|
+
private_class_method :operation_has_cocina_version?
|
38
|
+
|
39
|
+
# rubocop:disable Style/ClassVars
|
40
|
+
def self.root
|
41
|
+
@@root ||= OpenAPIParser.parse(YAML.load_file(openapi_path), strict_reference_validation: true)
|
42
|
+
end
|
43
|
+
# rubocop:enable Style/ClassVars
|
44
|
+
private_class_method :root
|
45
|
+
|
46
|
+
def self.openapi_path
|
47
|
+
::File.expand_path('../../../../openapi.yml', __dir__)
|
48
|
+
end
|
49
|
+
private_class_method :openapi_path
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Validators
|
6
|
+
# Perform validation against all other Validators
|
7
|
+
class Validator
|
8
|
+
VALIDATORS = [OpenApiValidator, DarkValidator, CatalogLinksValidator].freeze
|
9
|
+
|
10
|
+
def self.validate(clazz, attributes)
|
11
|
+
VALIDATORS.each { |validator| validator.validate(clazz, attributes) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -25,6 +25,15 @@ module Cocina
|
|
25
25
|
def self.properties
|
26
26
|
@properties ||= {}
|
27
27
|
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Returns the URI for the term `property` in this vocabulary.
|
31
|
+
#
|
32
|
+
# @param [#to_sym] property
|
33
|
+
# @return [String]
|
34
|
+
def self.[](property)
|
35
|
+
properties[property.to_sym]
|
36
|
+
end
|
28
37
|
end
|
29
38
|
end
|
30
39
|
end
|
data/openapi.yml
CHANGED
@@ -369,18 +369,28 @@ components:
|
|
369
369
|
CatalogLink:
|
370
370
|
type: object
|
371
371
|
additionalProperties: false
|
372
|
-
required:
|
373
|
-
- catalog
|
374
|
-
- catalogRecordId
|
375
372
|
properties:
|
376
373
|
catalog:
|
377
374
|
description: Catalog that is the source of the linked record.
|
378
375
|
type: string
|
376
|
+
enum:
|
377
|
+
- symphony
|
378
|
+
- previous symphony
|
379
379
|
example: symphony
|
380
|
+
refresh:
|
381
|
+
description: Only one of the catkeys should be designated for refreshing.
|
382
|
+
This means that this key is the one used to pull metadata from the catalog
|
383
|
+
if there is more than one key present.
|
384
|
+
type: boolean
|
385
|
+
default: false
|
380
386
|
catalogRecordId:
|
381
387
|
description: Record identifier that is unique within the context of the linked record's catalog.
|
382
388
|
type: string
|
383
389
|
example: '11403803'
|
390
|
+
required:
|
391
|
+
- catalog
|
392
|
+
- catalogRecordId
|
393
|
+
- refresh
|
384
394
|
CatkeyBarcode:
|
385
395
|
description: The barcode associated with a DRO object based on catkey, prefixed with 36105
|
386
396
|
type: string
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cocina-models
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.72.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Coyne
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-04-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -384,7 +384,10 @@ files:
|
|
384
384
|
- lib/cocina/models/title.rb
|
385
385
|
- lib/cocina/models/title_builder.rb
|
386
386
|
- lib/cocina/models/validatable.rb
|
387
|
-
- lib/cocina/models/
|
387
|
+
- lib/cocina/models/validators/catalog_links_validator.rb
|
388
|
+
- lib/cocina/models/validators/dark_validator.rb
|
389
|
+
- lib/cocina/models/validators/open_api_validator.rb
|
390
|
+
- lib/cocina/models/validators/validator.rb
|
388
391
|
- lib/cocina/models/version.rb
|
389
392
|
- lib/cocina/models/vocabulary.rb
|
390
393
|
- lib/cocina/models/world_access.rb
|
@@ -395,7 +398,7 @@ homepage: https://github.com/sul-dlss/cocina-models
|
|
395
398
|
licenses: []
|
396
399
|
metadata:
|
397
400
|
rubygems_mfa_required: 'true'
|
398
|
-
post_install_message:
|
401
|
+
post_install_message:
|
399
402
|
rdoc_options: []
|
400
403
|
require_paths:
|
401
404
|
- lib
|
@@ -411,7 +414,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
411
414
|
version: '0'
|
412
415
|
requirements: []
|
413
416
|
rubygems_version: 3.2.32
|
414
|
-
signing_key:
|
417
|
+
signing_key:
|
415
418
|
specification_version: 4
|
416
419
|
summary: Data models for the SDR
|
417
420
|
test_files: []
|
@@ -1,49 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Cocina
|
4
|
-
module Models
|
5
|
-
# Perform validation against openapi
|
6
|
-
class Validator
|
7
|
-
def self.validate(clazz, attributes)
|
8
|
-
method_name = clazz.name.split('::').last
|
9
|
-
request_operation = root.request_operation(:post, "/validate/#{method_name}")
|
10
|
-
|
11
|
-
# JSON.parse forces serialization of objects like DateTime.
|
12
|
-
json_attributes = JSON.parse(attributes.to_json)
|
13
|
-
# Inject cocinaVersion if needed and not present.
|
14
|
-
if operation_has_cocina_version?(request_operation) && !json_attributes.include?('cocinaVersion')
|
15
|
-
json_attributes['cocinaVersion'] = Cocina::Models::VERSION
|
16
|
-
end
|
17
|
-
|
18
|
-
request_operation.validate_request_body('application/json', json_attributes)
|
19
|
-
rescue OpenAPIParser::OpenAPIError => e
|
20
|
-
raise ValidationError, e.message
|
21
|
-
end
|
22
|
-
|
23
|
-
# rubocop:disable Metrics/AbcSize
|
24
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
25
|
-
def self.operation_has_cocina_version?(request_operation)
|
26
|
-
schema = request_operation.operation_object.request_body.content['application/json'].schema
|
27
|
-
all_of_properties = Array(schema.all_of&.flat_map { |all_of| all_of.properties&.keys }).compact
|
28
|
-
one_of_properties = Array(schema.one_of&.flat_map { |one_of| one_of.properties&.keys }).compact
|
29
|
-
properties = Array(schema.properties&.keys)
|
30
|
-
(properties + all_of_properties + one_of_properties).include?('cocinaVersion')
|
31
|
-
end
|
32
|
-
# rubocop:enable Metrics/AbcSize
|
33
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
34
|
-
private_class_method :operation_has_cocina_version?
|
35
|
-
|
36
|
-
# rubocop:disable Style/ClassVars
|
37
|
-
def self.root
|
38
|
-
@@root ||= OpenAPIParser.parse(YAML.load_file(openapi_path), strict_reference_validation: true)
|
39
|
-
end
|
40
|
-
# rubocop:enable Style/ClassVars
|
41
|
-
private_class_method :root
|
42
|
-
|
43
|
-
def self.openapi_path
|
44
|
-
::File.expand_path('../../../openapi.yml', __dir__)
|
45
|
-
end
|
46
|
-
private_class_method :openapi_path
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|