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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5058bc2927b8788ad9c04142decbfcc7cf42b7daa3b84665d64dd00b432d8ff4
4
- data.tar.gz: 524263003bc679432b5c120690775de4ebc14ef5fa6158337e0beff9c2bfb332
3
+ metadata.gz: b5516cbeff2267e688f6a09b34e4a0c715a1aea91ddae03d10060bed3d364c5a
4
+ data.tar.gz: f47bd2a4ca35c95d3566883707d39a2480c0b7e18ee28bc49042aa90b24e104c
5
5
  SHA512:
6
- metadata.gz: b84f98459177c74cc822a8dee105412c7023c3fda2e953e1f760c482ae3a2967f610207bb53cd33c510e2b0c861834b4cf1998459b70035ac9b22aa55c83b910
7
- data.tar.gz: '018baa839ef5774c3969f91acb9545fb04ddf3be652ae3f3fafbc6458ddd7ec1d7c4ae5d5d33a9a8eab36476519cd310a33b61da34e4f934bef35ef490dc83fd'
6
+ metadata.gz: 262cdfa4049c7325751645a578b2129f0f5cc4737198b82bea5728a6d693398bf2ab589dea2ae912de2de09e6abd1e3a581b84828f3ebf85ed64ba4c3a71dc25
7
+ data.tar.gz: 977677a8f5de44340117f3d2fb6ba8553e21c3c61c1770b18d44652008de6e26a6ada747de9546e993a0e4231e2263ad531d97491c5467d8aa8e18f2a303f340
@@ -78,7 +78,6 @@ module Cocina
78
78
  'rights_description_builder.rb',
79
79
  'title_builder.rb',
80
80
  'validatable.rb',
81
- 'validator.rb',
82
81
  'version.rb',
83
82
  'vocabulary.rb'
84
83
  ].freeze
@@ -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.structuredValue,
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 [Array<Cocina::Models::StructuredValue>] structured_values - the pieces of a structuredValue
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(structured_values, non_sorting_char_count)
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
- structured_values.each do |structured_value|
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
- non_sorting_size = [non_sorting_char_count - (value&.size || 0), 0].max
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(structured_values)
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 non_sorting_char_count(title)
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
- return 0 unless non_sort_note
174
-
175
- non_sort_note.value.to_i
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 && name
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 (validate || validate.nil?) && self.class.name
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
- VERSION = '0.71.0'
5
+ VERSION = '0.72.0'
6
6
  end
7
7
  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.71.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-03-31 00:00:00.000000000 Z
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/validator.rb
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